Code Story: How We Rewrote Our Legacy PHP 8 App in Python 3.13 – 3x Faster Development
Legacy codebases are a double-edged sword: they power critical business workflows, but as teams grow and requirements shift, they become harder to maintain, extend, and secure. Our team faced exactly this with a 5-year-old PHP 8 monolith that handled our core e-commerce order processing pipeline. After 18 months of struggling with slow feature velocity, we made the call to rewrite the entire application in Python 3.13. The result? A 3x faster development cycle, 40% lower latency, and a codebase that new engineers can onboard to in days instead of weeks.
Why We Left PHP 8
First, let’s be clear: PHP 8 is a capable, mature language. Our decision to move away wasn’t a knock on PHP, but a reflection of our team’s needs. Our original app was built in 2019 on PHP 7.4, upgraded to PHP 8.0 in 2021, then 8.2 in 2023. Over time, we hit three core pain points:
- Fragmented tooling: We relied on a mix of Laravel (for API routes), custom Symfony components (for order logic), and legacy raw PHP scripts for background jobs. Onboarding new devs required teaching three separate frameworks, plus our custom conventions.
- Slow testing cycles: Our PHP test suite took 22 minutes to run on CI, with flaky integration tests that broke 1 in 5 runs. Developers often skipped local testing, leading to more bugs in production.
- Limited async support: PHP 8’s Fibers are a step forward, but our team had more experience with Python’s asyncio ecosystem. We needed to handle 10x more concurrent background jobs (order fulfillment, inventory syncs) without rewriting our entire event loop.
Why Python 3.13?
Python 3.13 (released October 2024) was a key enabler for our rewrite. We evaluated Go, Rust, and Node.js, but Python won out for three reasons:
- Team familiarity: 80% of our engineering team already used Python for data pipelines and internal tooling. No need for a steep learning curve.
- 3.13-specific features: The new deferred evaluation for annotations, improved asyncio performance, and better type checking support via the built-in typing module cut down on boilerplate we’d have needed in older Python versions.
- Ecosystem fit: We needed libraries for Stripe integration, PostgreSQL ORMs, and background job processing. Python’s ecosystem had mature, well-maintained options (SQLAlchemy 2.0, FastAPI, Celery) that matched our needs out of the box.
Planning the Rewrite
We followed a strangler fig pattern to avoid a risky big-bang migration. Here’s our 6-month timeline:
- Month 1-2: Audit & Parallel Setup: We mapped all 142 API endpoints, 28 background job types, and 12 third-party integrations in the PHP app. We set up a parallel Python 3.13 repo with FastAPI, SQLAlchemy, and a matching staging environment.
- Month 3-4: Incremental Migration: We routed low-traffic endpoints (health checks, internal admin APIs) to the Python app first, then moved core order endpoints one by one. We used a shared PostgreSQL database during migration to avoid data sync issues.
- Month 5: Cutover & Decommission: Once 95% of traffic was routed to Python, we deprecated the PHP app, archived the repo, and redirected all remaining traffic.
Tooling & Stack Choices
We kept our stack lean to avoid bloat:
- Web framework: FastAPI (chosen over Django for lighter weight, built-in OpenAPI docs, and native asyncio support)
- ORM: SQLAlchemy 2.0 with asyncpg driver for PostgreSQL
- Background jobs: Celery with Redis broker, upgraded to use Python 3.13’s improved asyncio for 2x faster job processing
- Testing: Pytest with pytest-asyncio, cutting our test suite runtime from 22 minutes (PHP) to 4 minutes (Python)
- Type checking: Pyright (strict mode) to catch errors at build time, replacing PHP’s weaker static analysis tools
Results: 3x Faster Dev
We measured development velocity by tracking time from ticket creation to production deployment for new features. Before the rewrite, our average was 12 days per feature. After the rewrite? 4 days per feature – exactly 3x faster. Here’s why:
- Less boilerplate: Python 3.13’s type hints and FastAPI’s automatic request validation cut out 30% of the code we had to write for each new endpoint.
- Faster testing: Our 4-minute test suite meant developers could run full regression tests locally before pushing, reducing back-and-forth with QA.
- Better onboarding: New engineers now spend 3 days instead of 3 weeks getting up to speed, since the Python codebase follows standard PEP conventions and has unified tooling.
- Performance gains: The Python app has 40% lower p99 latency than the PHP app, and handles 2x more concurrent requests with the same infrastructure.
Lessons Learned
Rewrites are risky, and we made mistakes along the way. Here are our top takeaways:
- Don’t rewrite for the sake of it: We only made the call after quantifying the cost of maintaining the PHP app vs. rewriting. If your legacy app is stable and your team is productive, don’t rewrite.
- Use the strangler fig pattern: Never do a big-bang migration. Incremental routing let us catch issues early without downtime.
- Match tooling to your team: We almost chose Go for performance, but Python’s team familiarity saved us months of training time. Productivity beats raw performance for most use cases.
- Invest in type checking early: Pyright strict mode caught 40% of bugs before they hit CI, saving us countless hours of debugging.
Conclusion
Rewriting our legacy PHP 8 app in Python 3.13 was one of the best technical decisions our team made this year. We didn’t just get faster development – we got a more reliable, easier to maintain app that scales with our business. If you’re considering a legacy rewrite, start with a small pilot, measure everything, and prioritize your team’s needs over hype.
Top comments (0)