Get a quote

Migrating from PHP to Go in Production: Lessons from a Real Backend Rewrite

Most PHP-to-Go migrations fail not because Go is hard but because teams underestimate how tightly coupled a PHP monolith really is. We have run this migration in production and the lessons are not what you find in tutorials.

Running a PHP monolith in production for several years means accumulating a particular kind of technical debt that only becomes visible when you try to leave. The application works. It handles traffic. Engineers understand it well enough to ship features. And then one day the team decides the time has come to migrate to Go, expecting a clean rewrite to result in a faster, leaner system within six months.

That six-month estimate is almost always wrong. Not because Go is difficult but because the PHP monolith contains years of implicit contracts that nobody wrote down. This is an account of what we have seen in real PHP-to-Go migrations for backend systems serving Lebanese and MENA companies, and specifically what makes them succeed or fail.

Why teams decide to migrate from PHP to Go

The honest reasons for migrating to Go are rarely the ones stated in the planning document. The stated reasons are typically performance, type safety, and concurrency. The real reasons are usually one or more of the following.

The PHP application has reached a size where adding features feels dangerous. Nobody on the team fully understands what happens when a request arrives. Side effects are scattered across service classes, model hooks, middleware, and event listeners in ways that no single engineer can hold in their head.

The team is hiring engineers who know Go and want to work with Go. Retention is a real pressure in Lebanese and regional tech hiring.

A specific performance problem, usually around a high-traffic endpoint or a background processing pipeline, has convinced leadership that Go's concurrency model is the solution.

None of these reasons are wrong. They are all legitimate. But treating the migration as primarily a technical exercise misses the organizational and architectural work that dominates the actual effort.

The strangler fig pattern: why it is the right migration approach

A big-bang rewrite of a PHP monolith into Go is one of the highest-risk engineering decisions a team can make. The alternative is the strangler fig pattern: incrementally replace pieces of the PHP application with Go services while both systems run in production simultaneously.

The mechanics of strangler fig migration for PHP to Go look like this. A reverse proxy or API gateway sits in front of both the PHP monolith and the new Go services. Route by route, endpoint by endpoint, the proxy is reconfigured to send traffic to the Go service instead of the PHP handler. The PHP monolith shrinks as Go grows. When the PHP application serves zero traffic, it is retired.

This approach has real costs. Running two systems simultaneously is operationally complex. Engineers must maintain both codebases during the transition. Data consistency between the PHP side and the Go side requires careful coordination. But those costs are manageable and predictable. The costs of a failed big-bang rewrite are neither.

For MENA companies running systems with real users and real revenue, a production migration that fails halfway through is not acceptable. The strangler fig approach keeps both systems operational and reduces migration risk to a series of individually small deployable steps.

The first lesson: your database is the real monolith

Every team that starts a PHP-to-Go migration believing the database is just a detail will spend weeks correcting that belief. The PHP monolith and its database have co-evolved. The schema reflects PHP-isms. Timestamps are often stored as PHP's time() integer output or as formatted strings that PHP's date functions naturally produce. String columns contain serialized PHP data structures. Nullable columns exist because PHP's loose typing makes nullability easy to ignore.

The Go side cannot simply connect to the same database and start querying. It will immediately encounter columns whose types are technically valid SQL but whose values were written with PHP's interpretation in mind. A varchar column storing a:3:{i:0;s:5:"value";} is valid SQL. It is not valid Go.

The practical lesson is to treat the database migration as a first-class work stream, not an afterthought. Before any Go service can reliably replace a PHP endpoint, the schema needs an audit. Every column, every table, every index. The audit produces a list of columns that need type normalization, serialized fields that need migration to JSON or normalized tables, and implicit conventions that need to become explicit constraints.

This audit takes two to four weeks for a mid-sized PHP application schema. It is non-negotiable preparation time that most migration estimates skip.

Session and authentication coupling in PHP

PHP applications running in traditional server environments often use PHP's native session handling. Sessions are stored in files, in a Redis instance configured for PHP's session serialization format, or in a database using a PHP session handler. The session data is serialized in PHP's native format.

Go cannot read PHP session data natively. A Go authentication service cannot share session state with the PHP monolith without a translation layer. This creates a temporary but significant architectural problem: users authenticated on the PHP side cannot be transparently authenticated on the Go side, and vice versa, until you build the bridge.

The bridge typically looks like one of two things. Either you migrate session storage to a format both systems can read, typically JWT tokens or a shared Redis key format with a documented schema. Or you build a thin PHP authentication service that the Go side calls to validate sessions during the transition period.

Of these two options, JWT migration is cleaner long-term but requires re-authenticating all existing sessions during the cutover. For applications where users are always re-authenticating regularly, this is invisible. For applications with long-lived sessions, it is a noticeable disruption that requires user communication.

The PHP timezone and datetime nightmare

PHP's date and time handling is a reliable source of migration bugs. PHP's date() function uses the server's timezone unless explicitly overridden. Many PHP applications do not set date_default_timezone_set() and rely on whatever the server default is. Timestamps stored in the database may be in local time, UTC, or some inconsistent mix depending on which server generated them.

Go's time package defaults to UTC and is explicit about timezone handling. When the Go service starts reading timestamps that the PHP monolith wrote, it will frequently encounter timestamps that are technically valid but whose timezone interpretation is wrong.

Audit timestamp storage before the migration starts. Establish a rule: all timestamps going forward are stored as UTC in the database. Write a migration that converts existing timestamps. Enforce UTC in Go with time.UTC everywhere. This work front-loads pain that, if deferred, will produce subtle bugs in date-sensitive features such as scheduling, reporting, billing, and notification timing.

Performance expectations: what actually improves and what does not

Go is genuinely faster than PHP for CPU-bound work and concurrent request handling. The performance improvement in a production migration is real but narrower than most teams expect.

For pure request handling under concurrent load, Go services typically show 3x to 10x improvement in requests per second compared to equivalent PHP code running without OPcache. Under normal production traffic at typical MENA SaaS scale, this translates to being able to handle more concurrent users on the same infrastructure at lower per-request latency.

For database-heavy endpoints where 80% of request time is PostgreSQL query execution, the improvement is closer to 10% to 30%. Go schedules the database wait more efficiently than PHP's synchronous model but the query itself takes the same time.

For background jobs that were previously scheduled via PHP cron processes, Go's goroutine concurrency typically allows 5x to 20x more concurrent job execution on the same hardware. This is where Go's concurrency model delivers its most dramatic visible benefit in practice.

Go's memory usage is also significantly lower than PHP's per-request model. A PHP request that forks a new process or reuses a worker allocates several megabytes per request. A Go handler spawning a goroutine allocates a few kilobytes of stack space that grows only as needed.

Migration timeline reality check

For a PHP monolith that has been in active development for three or more years, a realistic Go migration timeline under the strangler fig approach looks like this.

Weeks one through four: database audit, schema normalization plan, authentication layer design. No Go code written yet.

Weeks five through twelve: Go project structure, shared internal packages, first high-value endpoint migrated and in production. Infrastructure for running Go alongside PHP.

Months four through eight: systematic migration of endpoints, with the highest-traffic and least complex endpoints going first. More complex business logic endpoints deferred.

Months nine through fourteen: migration of the most complex PHP code, including areas with heavy ORM magic, event system logic, and background job processing.

Month fifteen onward: PHP is retired when traffic share drops below 1%, with a final migration period for edge cases.

Teams that plan for three to six months and find themselves at month fourteen are not failing. They are discovering the true scope of the work, which was always there but was not visible at the start.

When not to migrate to Go

Not every PHP application should migrate to Go. If the application is stable, the team knows it deeply, and the primary business problem is not one that Go's characteristics directly solve, the migration is a cost with unclear return.

The migration makes sense when performance problems are specifically related to concurrent request handling or CPU-bound work, when the team is growing and Go is the language of new hires, or when the application is being redesigned significantly and the rewrite is incremental rather than big-bang.

The migration rarely makes sense when the PHP application is primarily doing CRUD over a database and the performance is already acceptable, when the engineering team is fully PHP-fluent and Go would require months of learning before the first productive contribution, or when the company is at an early stage where the bottleneck is product-market fit rather than infrastructure performance.


Key lessons from production

The database is the real migration challenge. Audit it before writing any Go.

Session and authentication require explicit bridge architecture during the transition period.

Timezones in PHP code are often implicit. Make them explicit before the migration.

Performance improvements are real but concentrated in specific use cases. Measure first, then decide which endpoints to migrate first.

The strangler fig approach is not optional for production systems with real users.

Timelines require a 2x to 3x buffer over initial estimates because the implicit contracts in a PHP monolith take time to surface.

Free PDF Download

Enjoying this article?

Enter your email and get a clean, formatted PDF of this article - free, no spam.

Free. No spam. Unsubscribe any time.

Not sure where to start?

Voxire builds and migrates backend systems for companies in Lebanon and the MENA region. If your team is evaluating a PHP-to-Go migration or needs a production-safe migration plan, reach out.

https://voxire.com/get-a-quote/

Back to blog
Chat on WhatsApp