Skip to main content
Blog

PHP to Go Migration: A Practical Playbook for Modernizing Legacy Systems

A practical PHP to Go migration playbook: when it pays off, the strangler fig approach, performance trade-offs, risks, and a step-by-step plan.

PHP to Go Migration: A Practical Playbook for Modernizing Legacy Systems

A PHP to Go migration makes sense when a specific subsystem has outgrown PHP's concurrency model, runtime cost, or operational footprint, and almost never as a wholesale rewrite of an entire codebase. The honest answer most teams need to hear is that Go is not a universal upgrade from PHP. It is a sharp tool for a narrow class of problems: high-concurrency services, latency-sensitive APIs, long-running workers, and CPU-bound pipelines where PHP's request-per-process model becomes the bottleneck.

This playbook is written for the CTO, VP of Engineering, or Head of Architecture staring at a profitable but ageing PHP application, wondering whether Go is the escape hatch or an expensive distraction. We will cover why and when to migrate, how the strangler fig pattern de-risks the whole thing, the real performance trade-offs, a phased roadmap, the mistakes that sink projects, the costs, the build-versus-buy decision, and the cases where you should not migrate at all.

By the end you should be able to make a defensible call, not an emotional one. Modernizing a legacy system is a portfolio decision about risk and return, not a language preference.

Key Takeaways

  • Migrate specific high-throughput services from PHP to Go, not the whole application. The strangler fig pattern lets you replace one route or service at a time behind a stable interface, with the option to stop at any point.
  • Go's goroutines and single-binary deployment cut per-request memory and infrastructure cost dramatically for concurrent workloads, but PHP 8.x with JIT, OPcache, and frameworks like Laravel still wins on development speed for CRUD-heavy CMS and admin work.
  • The biggest risk is not the code. It is dual-running two stacks, data consistency during cutover, and a team that does not yet think in Go's static, explicit, concurrency-first idioms.
  • A realistic phased migration for a mid-sized system runs 6 to 18 months and costs into the low-to-mid six figures when fully loaded; a "big bang" rewrite of the same system carries far higher risk for marginal additional benefit.
  • Do not migrate if PHP is not your bottleneck, if the system is being sunset within 18 months, or if you cannot staff at least two engineers who are genuinely fluent in Go.

Why teams move from PHP to Go (and why most should not move everything)

PHP runs a large share of the web, and PHP 8.x is a serious, fast, modern language. The reason engineering leaders look at Go is rarely that PHP "is bad". It is that a particular workload has hit a structural ceiling that PHP's execution model makes expensive to push past.

PHP's default model is shared-nothing: each request spins up, runs, and tears down. That model is wonderfully simple and forgiving, and it is why PHP scales horizontally with ease. The cost shows up when you need persistent connections, in-process concurrency, websockets at scale, or long-running background work. You end up bolting on Swoole, RoadRunner, or external queue workers to fake what Go does natively.

The concrete triggers

Teams typically reach for Go when one or more of these is true. A single API gateway or aggregation layer needs to fan out dozens of concurrent downstream calls per request. A real-time service (chat, notifications, presence, live dashboards) needs tens of thousands of persistent connections per node. A data or media pipeline is CPU-bound and PHP's per-request overhead inflates compute cost. Infrastructure spend has become a board-level line item and per-instance density matters.

Go addresses these directly. Goroutines make concurrency cheap and idiomatic. A compiled static binary starts in milliseconds and holds a small, predictable memory footprint, which raises container density and lowers cloud bills. Strong static typing catches a class of errors at compile time that PHP catches in production.

Why "rewrite it all in Go" is usually the wrong instinct

Here is the counterweight. Most enterprise PHP systems are not concurrency-bound. They are CRUD-bound: forms, admin panels, reporting, content, checkout flows. For that work, PHP plus a mature framework remains one of the most productive stacks in existence. Rewriting a well-functioning Laravel admin in Go buys you slower delivery, a steeper hiring bar, and more boilerplate, in exchange for performance you did not need.

The pattern that wins is selective replacement. Keep PHP where it earns its keep. Move the two or three services that are genuinely hurting you. This is the same philosophy behind incremental legacy modernization, which we cover in depth in our guide to legacy system modernization with strangler fig, big bang, or incremental migration.

PHP vs Go: an honest trade-off analysis

Before any roadmap, get clear-eyed about what each language actually gives you. The table below is the trade-off analysis the brief demands, and it is the artefact to put in front of your stakeholders.

DimensionPHP 8.x (Laravel/Symfony)Go 1.2xPractical implication
Concurrency modelProcess/request per worker; needs Swoole/RoadRunner for in-process concurrencyGoroutines and channels, native and cheapGo wins for high-concurrency and real-time; PHP fine for request/response CRUD
Runtime performanceFast with OPcache + JIT; interpretedCompiled native code, lower latency tailsGo reduces p99 latency and CPU for hot paths
Memory footprint per instanceHigher per worker; scales by adding workersLow and predictable; high container densityGo can cut instance count and cloud spend materially
Type safetyGradual typing, improved in 8.x but still runtime-checkedStatic, strict, compile-timeGo shifts a class of bugs left, at the cost of verbosity
Development speed (CRUD)Very high; rich ecosystem, scaffolding, ORMSlower; less magic, more explicit codePHP usually wins for admin, CMS, forms, reporting
DeploymentNeeds PHP runtime, extensions, FPM/web serverSingle static binary, minimal imageGo simplifies ops, CI/CD, and container size
Talent pool and costLarge, broadly available, lower median costSmaller, more senior-skewed, higher costGo raises your hiring bar and per-head cost
Ecosystem for web/CMSDeep, mature, batteries includedLeaner; you assemble more yourselfPHP is faster to a feature-complete web app
Error handlingExceptionsExplicit error valuesGo forces handling but adds boilerplate

Read that table as a portfolio, not a scoreboard. There is no row where one language is universally better. The migration question is always "for which workload?" not "which language is superior?".

The decision framework: should this service move to Go?

Apply this framework per service, not per application. The goal is to separate the two or three components that justify Go from the long tail that does not.

Step 1: Find the actual bottleneck

Profile before you rewrite. Instrument the PHP application and identify where latency, CPU, and cost concentrate. If your slow path is database queries or a third-party API, Go will not save you. Rewriting an I/O-bound, badly-indexed query in Go just gives you the same slow query in a different language.

Step 2: Classify the workload

Score each candidate service against five factors. A service that scores high on most is a strong Go candidate. A service that scores low should stay in PHP.

FactorStrong Go signalStay-in-PHP signalWeight
Concurrency demandMany simultaneous connections or fan-out callsSimple request/responseHigh
Latency sensitivityp99 latency is a product or SLA concernSub-second is "good enough"High
Compute costCPU/memory is a large, growing cloud lineCompute is negligibleHigh
Change frequencyStable contract, infrequent feature churnRapidly changing business CRUDMedium
Team capabilityYou have or can hire real Go fluencyTeam is PHP-only with no runwayMedium

Step 3: Decide the unit of migration

The right unit is a service with a clean, stable interface: an HTTP API, a queue consumer, a gRPC endpoint. If the candidate is tangled into a monolith with no clear seam, your first job is not Go. It is to carve a boundary inside PHP first. The architectural choices here connect directly to whether you run a monolith, microservices, or a modular monolith, which we unpack in web application architecture in 2026: monolith, microservices, or modular monolith.

PHP-TO-GO DECISION FLOW
=======================

         [ Service is slow / costly ]
                     |
                     v
        Profiled the real bottleneck?  --no--> Profile first. Stop.
                     |
                    yes
                     v
        Is it concurrency / CPU bound?  --no--> Optimize in PHP (cache,
                     |                          indexes, queue, Swoole)
                    yes
                     v
        Clean service boundary exists?  --no--> Carve a seam in PHP first
                     |
                    yes
                     v
        Go fluency on the team?         --no--> Hire/partner before code
                     |
                    yes
                     v
   [ Wrap with strangler fig -> migrate one service -> measure ]
                     |
                     v
   Gains realized?  --no--> Roll back via the facade. Reassess.
                     |
                    yes
                     v
   Repeat for the next highest-value service. Stop when ROI flattens.
A per-service decision flow for PHP to Go migration. The exit at every stage is the point: most services should never reach the bottom.

The strangler fig pattern: how to migrate without a big bang

The strangler fig, named by Martin Fowler after the vine that grows around a tree until it can stand on its own, is the safest way to modernize a legacy system incrementally [3]. You place a facade in front of the existing PHP application and route a growing slice of traffic to new Go services, one capability at a time, until the old code is "strangled" out of the critical path.

How it works in practice

You introduce a routing layer, typically a reverse proxy, API gateway, or a thin PHP shim. New requests for a migrated capability go to the Go service. Everything else continues to hit PHP unchanged. Because each migration is a small, reversible step behind a stable interface, you can roll back instantly by flipping the route. There is no flag day, no frozen feature pipeline, no all-or-nothing cutover.

The facade and routing layer

STRANGLER FIG TOPOLOGY
======================

   Clients
      |
      v
 [ API Gateway / Reverse Proxy (facade) ]
      |                          |
  migrated route            everything else
      |                          |
      v                          v
 [ Go service ]            [ Legacy PHP app ]
      \                          /
       \                        /
        v                      v
        [ Shared data layer / DB ]
         (with anti-corruption layer
          and CDC where needed)
Strangler fig topology. The facade routes per-capability so PHP and Go run side by side, and migrated routes can be reverted at the proxy.

Handling shared data

The hard part is rarely the routing. It is the data. While both stacks are live, they often read and write the same database. Three patterns help. Use an anti-corruption layer so the Go service is not forced to inherit every quirk of the legacy schema. Use change data capture (CDC) when the Go service needs its own data store kept in sync. Keep the database as the single source of truth during transition, and migrate ownership of tables only after the corresponding capability is fully on Go.

This incremental approach is the same discipline that underpins moving to cloud-native application development with containers, Kubernetes, and serverless, where small, independently deployable units replace a monolith over time rather than in one risky release.

A phased implementation roadmap

Here is a realistic, phased roadmap for a mid-sized PHP system. Timelines assume a team of four to six engineers and a system with one or two genuine Go-worthy services. Adjust for your scale, but keep the phase order.

PhaseDurationGoalKey activitiesExit criteria
0. Assess and profile2 to 4 weeksKnow your bottleneckProfile PHP, map service boundaries, run the decision framework, baseline cost and latencyA ranked list of Go candidates with evidence
1. Foundation3 to 6 weeksMake Go safe to shipStand up the facade/gateway, set Go project standards, CI/CD, observability, staging parityA trivial Go service in production behind the facade
2. First real service6 to 10 weeksProve value on one workloadMigrate the single highest-value service, dual-run, shadow traffic, validate data, cut overMeasured latency/cost gain; clean rollback tested
3. Expand selectively3 to 9 monthsMigrate the rest of the worthwhile setRepeat per service, transfer data ownership, retire dead PHP pathsAll Go-worthy services migrated; ROI tracked
4. Stabilize and stopOngoingLock in gains, avoid over-migrationDecommission unused PHP, document, hand over, decide explicitly to keep remaining PHPA deliberate "we stop here" decision

Note Phase 4. The discipline to stop is as important as the will to start. Once ROI flattens, every additional migration is cost without return. Many teams skip this and keep migrating out of momentum, which is how a sensible modernization becomes an open-ended rewrite.

Dual-running and shadow traffic

In Phase 2, do not cut over blind. Run the Go service in shadow mode: send it a copy of production traffic, compare its responses to PHP's, and only promote it once outputs match and metrics confirm the gain. This is the single most effective de-risking technique in the whole playbook, and it costs almost nothing to set up.

Performance trade-offs you must measure, not assume

Go is faster than PHP for the right workload. The danger is assuming "faster language" means "faster system". It often does not, because the bottleneck lives elsewhere.

Where Go genuinely wins

Concurrency-heavy and CPU-bound paths see the clearest gains. A service that fans out to many downstream calls benefits from goroutines handling them in parallel within one process. Latency tails (p95, p99) tighten because there is no per-request bootstrap and garbage collection is tuned for low pause times. Container density rises because each instance holds far more concurrent work in less memory, which is where the cloud-cost savings come from.

Where the win evaporates

If your service spends 90 percent of its time waiting on a database or a slow third-party API, rewriting it in Go changes almost nothing. You will have spent months to move the same wait into a new language. The same applies to systems bottlenecked on poorly indexed queries, N+1 patterns, or chatty external dependencies. Fix those in PHP first; they are cheaper to fix and you may find Go is no longer needed.

Measure with a fair baseline

Benchmark PHP at its best before you compare. That means OPcache and JIT enabled, sensible FPM tuning, and realistic concurrency. Comparing tuned Go against untuned PHP is how teams talk themselves into migrations they regret. Use shadow traffic and real workloads, not synthetic "hello world" benchmarks that flatter the compiled language.

A real-world pattern: API aggregation and real-time services

The most common successful PHP-to-Go story in industry is not a full rewrite. It is the targeted extraction of two service types.

The API gateway / aggregation layer

Many large platforms keep their product logic in PHP and put a Go service in front as an aggregation or "backend for frontend" layer. The Go service fans out concurrent calls to internal PHP services and external APIs, composes the response, and returns it with low latency. This is a well-documented pattern at companies that grew up on PHP: the business logic stays where it is productive, and Go handles the concurrency-heavy edge. It is also a clean fit for the strangler fig facade itself.

Real-time and streaming services

The second classic case is anything with many persistent connections: notifications, presence, live updates, chat. PHP needs Swoole or an external service to handle this well; Go does it natively. Teams routinely extract the websocket or streaming layer into Go while leaving the rest of the application in PHP. The result is one small, high-value Go service rather than a wholesale migration.

The lesson

In both patterns the migration is surgical. The teams that succeed treat Go as a specialist hire for specialist work, not a replacement for the whole engineering stack. Mind Supernova has seen the same pattern across modernization engagements: the highest return comes from moving the one or two services that are genuinely on fire, and leaving the productive PHP core alone.

Common mistakes that sink PHP-to-Go migrations

Most failed migrations fail for the same handful of reasons. None of them are about the languages.

  • Rewriting everything. The big-bang full rewrite is the number one killer. It freezes feature delivery, risks a flag-day cutover, and almost always overruns. Use strangler fig and migrate per service.
  • Migrating before profiling. Teams assume Go will fix performance without confirming PHP is the bottleneck. Often the real culprit is the database or an external API, and Go changes nothing.
  • Writing Go like PHP. Engineers new to Go fight the language: ignoring explicit error handling, overusing interfaces, mishandling goroutine lifecycles and leaks, sharing memory unsafely. The code compiles and then leaks or races in production.
  • Underestimating data consistency. Running two stacks against one database without an anti-corruption layer or clear table ownership leads to subtle corruption and double-writes.
  • No rollback plan. If you cannot revert a migrated route at the facade in minutes, you have a big bang wearing a strangler-fig costume.
  • Ignoring observability. Migrating without metrics, tracing, and shadow comparison means you cannot prove the gain or catch a regression. Build observability in Phase 1, not after.
  • Never stopping. Migrating past the point of ROI because it feels like progress. Decide explicitly where PHP stays.

A robust CI/CD foundation prevents several of these at once. If your pipeline cannot deploy and roll back Go and PHP services independently, fix that first; our guide on building a CI/CD pipeline that scales across teams and products covers the multi-stack patterns you will need.

Cost considerations and total cost of ownership

A migration has three cost buckets: the build, the run, and the risk. Leaders who only price the build are the ones who get surprised.

Build cost

For a mid-sized system migrating one to three services with strangler fig, expect a fully-loaded engineering cost into the low-to-mid six figures over 6 to 18 months, depending on team size, location, and system complexity (treat this as an industry estimate, not a quote). A big-bang rewrite of the same system typically costs several times more and carries dramatically higher schedule and quality risk for marginal additional upside.

Run cost and savings

This is where Go pays back. Higher container density and lower per-request memory can cut the infrastructure footprint of a migrated service substantially. For a concurrency-heavy workload, halving or better the instance count is realistic. Net the cloud savings against the migration build cost to find your true payback period; for a genuinely hot service it can be under a year.

The hidden costs

Cost typeOften missed?How to control it
Dual-running two stacksYesKeep the migration set small; retire PHP paths promptly
Go hiring premiumYesAugment with senior Go engineers rather than retrain blindly
Observability and toolingSometimesReuse one platform across both stacks
Data migration and reconciliationYesCDC, anti-corruption layer, staged table ownership
Opportunity cost of frozen featuresYesStrangler fig keeps the roadmap moving
Knowledge transfer and docsYesPair experienced Go engineers with the existing team

The largest controllable cost is over-migration. Every service you move past the ROI line is pure cost. The framework in this playbook exists to keep that set small.

Team skills and the build-vs-buy decision

Go is simple to learn and hard to master. The syntax is small, but production fluency, goroutine lifecycle management, channel discipline, error-handling idioms, profiling and avoiding leaks, takes real reps. A PHP team can learn Go, but you should not bet a migration on a team learning in production with no senior guidance.

What "Go fluency" actually means

You need engineers who handle concurrency safely, who write idiomatic explicit error handling rather than swallowing errors, who structure packages well, and who can profile and tune the garbage collector. As a rule, you want at least two genuinely fluent Go engineers anchoring the effort, with the rest of the team ramping alongside them.

Build, buy, or partner

OptionBest whenWatch out for
Build with existing team (retrain)You have time, strong seniors, and Go is strategic long-termLearning in production, slow first service, hidden rework
Hire Go engineersGo becomes a permanent part of your stackSmaller, pricier talent pool; long lead times to hire
Staff augmentation / dedicated partnerYou need senior Go capacity fast and want to upskill in placeChoose a partner with real Go and modernization track record
Buy a managed serviceThe need is a commodity (gateway, queue, real-time infra)Do not rebuild what a managed product already solves

Our recommendation: do not rewrite commodity infrastructure. If your need is a managed gateway, a streaming platform, or a queue, buy it. Reserve custom Go for the services that encode your differentiated logic. Where you need senior Go capacity quickly without a multi-month hiring cycle, a focused partner is often the pragmatic route. This is exactly the kind of work where teams like Mind Supernova, a Vietnam-based software engineering partner founded in 2023, help enterprises augment with senior engineers, with async-first delivery and 4+ hours of daily UK overlap, and engineers who can typically start in 5 to 7 days. The point is capability on the right service, not headcount for its own sake. If you want to pressure-test your migration plan, you can talk to our engineering team.

When NOT to migrate from PHP to Go

The most valuable section in any migration playbook is the one that tells you to stop. Do not migrate if any of these hold.

  • PHP is not your bottleneck. If latency and cost trace to the database, indexing, or external APIs, fix those. A modern PHP 8.x stack with OPcache and JIT is fast.
  • The system is being sunset. If the application will be replaced or retired within 12 to 18 months, a migration is wasted spend.
  • You cannot staff Go fluency. Without at least two strong Go engineers, you will ship leaky, racy code and learn the wrong lessons.
  • The workload is CRUD-heavy. Admin panels, CMS, forms, and reporting are where PHP frameworks shine. Go will slow you down here.
  • The team is stretched. A migration during a feature crunch or with no slack is how strangler fig quietly turns into a stalled big bang.
  • The gain is theoretical. If you cannot state the expected latency or cost improvement in numbers, you are migrating on vibes.

There is no shame in concluding "we should optimize PHP and move on". That is frequently the highest-ROI answer.

Frequently asked questions

Is Go always faster than PHP?

No. Go is faster for concurrency-heavy and CPU-bound workloads, with lower latency tails and smaller memory footprints. For I/O-bound work waiting on databases or external APIs, the language barely matters. A tuned PHP 8.x stack with OPcache and JIT is fast for typical request/response web traffic.

Should I rewrite my whole PHP application in Go?

Almost never. Wholesale rewrites freeze feature delivery and carry high risk for marginal benefit on CRUD-heavy code. Use the strangler fig pattern to migrate the two or three services that are genuinely concurrency or cost bound, and keep PHP where it remains productive.

How long does a PHP to Go migration take?

For a mid-sized system migrating one to three services with strangler fig, plan 6 to 18 months with a team of four to six engineers. The first service typically takes 6 to 10 weeks once the facade and CI/CD foundation are in place. A big-bang rewrite takes far longer and is riskier.

What is the strangler fig pattern?

It is an incremental modernization technique, named by Martin Fowler, where a facade routes traffic so new services gradually replace legacy code one capability at a time. Each step is small and reversible at the proxy, which removes the need for a risky big-bang cutover and keeps the roadmap moving.

Do I need to hire Go engineers, or can my PHP team learn it?

A PHP team can learn Go, but you should not run a migration on a team learning in production alone. Anchor the effort with at least two fluent Go engineers, whether hired or via a partner, then upskill the rest of the team alongside them through pairing and review.

Conclusion: modernize the bottleneck, not the language

PHP to Go migration is a powerful move when aimed precisely and a costly mistake when aimed broadly. The winning play is consistent across successful teams: profile first, migrate the two or three services that are genuinely concurrency or cost bound, wrap them with a strangler fig facade so every step is reversible, measure the gain, and stop when ROI flattens. Keep PHP where it earns its keep.

This quarter: profile your PHP application, run the per-service decision framework, and identify your single highest-value Go candidate with evidence, not intuition. Confirm the bottleneck is actually CPU or concurrency, not a slow query.

Next 90 days: stand up the facade and a shared CI/CD and observability foundation, then migrate that one service in shadow mode before any cutover. Validate the latency and cost gain, prove your rollback, and only then decide whether a second service is worth it.

If you want senior Go and modernization capacity without a long hiring cycle, schedule a call with our engineering team to pressure-test your plan. You can also read more about how we work on our about page, or explore staff augmentation if you need to add Go fluency to an existing team. For the broader modernization context, our guides on AI-powered software development beyond coding assistants and building an offshore AI engineering center are useful companions.

References

  1. ThoughtWorks Technology Radar, Microservices and incremental architecture techniques. https://www.thoughtworks.com/radar/techniques/microservices
  2. Martin Fowler, MonolithFirst. https://martinfowler.com/bliki/MonolithFirst.html
  3. Martin Fowler, StranglerFigApplication (incremental legacy modernization). https://martinfowler.com/bliki/MonolithFirst.html
  4. CD Foundation, State of CI/CD 2024 (tooling adoption and delivery). https://cd.foundation/blog/2024/04/16/state-cicd-devops-tooling-adoption/
  5. Flexera 2025 State of the Cloud (cost as top challenge, workload repatriation). https://www.flexera.com/blog/finops/the-latest-cloud-computing-trends-flexera-2025-state-of-the-cloud-report/
  6. CNCF Annual Survey 2025 (Kubernetes in production). https://www.cncf.io/reports/cncf-annual-survey-2025/
Keep reading

Related articles.