Queue workers eating the cluster
Worker processes consume far more CPU and memory than the work justifies, and adding capacity means scaling the whole PHP runtime along with it.
PHP to Go migration · assessment, planning, implementation
Your PHP monolith does not necessarily need to become Go. I identify where PHP should stay, where modernization is enough, and where extracting a workload into Go produces a measurable engineering benefit.
Senior PHP and Go backend engineering: architecture assessment, migration planning, implementation and production rollout.
Serbia-based · CET/CEST · B2B contract · EUR invoicing · English communication
These are the symptoms that usually put a migration on the agenda. Most of them have more than one possible cause, which is exactly why the first step is measurement rather than a language decision.
Worker processes consume far more CPU and memory than the work justifies, and adding capacity means scaling the whole PHP runtime along with it.
Imports, exports and report generation run for minutes or hours inside a runtime built around short request lifecycles.
Work that spends most of its time waiting on other systems still needs one process per in-flight operation, so concurrency is paid for in RAM.
Jobs die quietly, retries duplicate side effects, and someone checks the queue by hand after every release.
A handful of routes carry a tight p99 budget that framework boot and per-request setup keep eating into.
The answer to every load problem is more application servers, because no single component can scale on its own.
A PHP version or framework major that nobody has upgraded, which turns every dependency bump into its own project.
Releases are slow, partly manual, and risky enough that they happen outside business hours.
Everything reaches into everything, so a change to billing can break search and nobody can predict the blast radius.
Feature estimates are dominated by working around the current structure rather than by building the feature.
The first question is whether a rewrite is necessary at all. Some PHP systems need architectural cleanup, a runtime upgrade, database work or better caching, and once that is done the pressure to migrate disappears along with the latency. That work is cheaper and easier to reverse, which is why it is worth knowing what to measure and fix in PHP before a rewrite is on the table.
Other systems contain a small number of workloads where Go's concurrency model, runtime characteristics or deployment model give a clear advantage. The point of the assessment is to identify those specific workloads before anyone commits to a migration, instead of treating the language itself as the problem. If you want the underlying comparison first, I have written about where PHP and Go genuinely differ as backend runtimes.
Keep PHP
Consider Go
The migration runs as a loop rather than a project with a cutover date. Each pass through it is small enough to stop after.
This is the strangler pattern applied to a PHP monolith: the old system keeps running while new implementations take over one responsibility at a time. The longer technical write-up is in the PHP to Go migration guide. What follows is how an engagement is actually sequenced.
Measure where CPU, memory, database time and wall-clock time actually go, in production, under real traffic rather than on a laptop.
Which code paths touch which tables, queues, caches and third-party APIs. This is usually the step that changes people's minds about scope.
Throughput, p50/p95/p99 latency, error rate, queue depth and cost per unit of work, recorded before anything changes.
One workload with a clear boundary, measurable pain and a small blast radius if it has to be reversed.
The API or event contract between PHP and the new component, including which side owns the data it reads and writes.
Implement the workload in Go against the agreed contract, with tests driven by real payload and message shapes taken from production.
Shadow traffic where the output can be compared, then a canary share, then the remainder. Queue consumers can simply run side by side.
Compare the new numbers with the ones recorded up front. If the improvement is not visible in the metrics, it is not an improvement.
Exercise the rollback path instead of trusting that the flag works. A step you cannot reverse is not finished.
Review whether a second workload is justified. Stopping after one is a legitimate outcome, not a failed migration.
Fixed price
€1,200Migration Readiness Audit · one-off
Discovery with a defined scope and a written deliverable. No commitment to any implementation work afterwards.
The recommendation may well be that no migration is justified. That is a useful result: it closes an open architectural question with evidence instead of opinion.
The pilot is a single workload on a hot path, real traffic, real data, real consequences, chosen because it is measurable and reversible. One workload in production tells you more than any proof of concept: you find out how the boundary behaves under load, how the deployment fits your infrastructure, and whether the improvement shows up in the metrics you agreed to watch.
The consumer whose throughput or resource use is the current constraint. Both implementations can read the same queue while traffic shifts.
A cron or scheduler job whose runtime has grown past the window it is supposed to finish in.
A single high-traffic or latency-sensitive route, placed behind the router or gateway you already run.
The component that calls several third-party systems and spends most of its life waiting on them.
Batch or streaming transformation work with a well-defined input and output.
A long-running component that does not fit the request and response model at all.
Queue consumers are the most common starting point, because both implementations can read the same queue and the traffic split is only a question of how many messages each side takes. The mechanics are covered in moving PHP queue workers to Go. If the discussion inside your team is really about splitting the system up rather than changing language, then whether a PHP monolith should become microservices is the more useful question to settle first.
Framework-specific notes for the two most common cases: Symfony to Go migration and Laravel to Go migration.
Implementation from €550/day after discovery. Migration implementation is scoped once the audit has established what moves and in what order. There is no fixed price for rewriting a whole monolith, because nobody can estimate that honestly before the dependency map exists.
Senior enough to make the architecture decision. Small enough to work directly. Structured enough for European B2B procurement.
The person who assesses the architecture is the person who writes the migration code. Nothing is sold by one party and delivered by another.
Deciding what stays and what moves needs both sides of the comparison. A Go specialist who cannot read your Symfony code will always recommend Go.
Migration plans written by someone who will not implement them tend to skip the parts that turn out to be hard.
You talk to the engineer, in your channels, about the actual system. Nobody relays requirements second-hand.
An audit first, then one workload. Each step is small enough to stop after without leaving stranded work behind.
One B2B agreement, EUR invoicing, and the standard documents European companies need before an external engineer starts.
Working stack
Remote from Serbia, working hours overlapping CET/CEST. Engagements run on one B2B agreement with a monthly invoice in EUR.
Almost never, and certainly not as a first move. A full rewrite means running two systems in parallel, freezing product work, and rediscovering years of undocumented business rules that exist only in the current code. The parts of a PHP monolith that actually hurt are usually a small subset: a few workers, a couple of endpoints, one integration that fans out. Moving those to Go while the monolith keeps serving everything else gives you most of the benefit for a fraction of the risk. If the rest of the system is stable and cheap to run, leaving it in PHP is the correct engineering decision rather than a compromise.
When the workload profile is the problem, not the code quality. Go earns its place where you need many concurrent in-flight operations inside one process, long-lived connections, predictable memory under sustained load, or a single static binary that starts in milliseconds. The request-per-process model that PHP is built around makes those workloads expensive, because you scale by adding processes and every process carries a full interpreter and framework boot. Good candidates are queue consumers, fan-out API aggregation, streaming and data pipelines, and latency-sensitive services with a tight p99 budget. If the bottleneck is a missing index or an N+1 query, Go will not fix it: it will run the same bad query faster.
Yes, and that is usually the end state rather than a transition phase. The two sides communicate over the same boundaries you would use for any service: HTTP or gRPC calls, a shared queue such as Redis, RabbitMQ or SQS, or events on a topic. In practice the monolith stays the system of record and the Go component takes over one job behind a clear contract. Sharing a PostgreSQL or MySQL schema directly is workable early on but should be treated as temporary, because two writers on one schema removes the boundary you were trying to create. Deployment stays separate, so the Go component can be rolled back without touching PHP.
Often that is the whole engagement. Upgrading the runtime to a current PHP 8.x release, replacing an abandoned framework layer, configuring OPcache and preloading properly, fixing the queries that dominate database time, and putting real caching in front of expensive reads will frequently produce a larger improvement than any rewrite, for a fraction of the cost. It also leaves you with one system to operate instead of two. I would rather tell you that your PHP is fine and your database needs attention than sell you a migration you do not need.
No. Extracting a workload is not the same as adopting microservices. What usually makes sense is a monolith plus a small number of services that exist for a concrete operational reason: independent scaling, isolation of a risky workload, or a genuinely different runtime profile. A fleet of small services brings distributed transactions, new network failure modes and per-service operational overhead, and teams that go there without platform capacity generally regret it. If you cannot name the reason a component must deploy and scale on its own, it belongs in the monolith.
By never making the cutover a single irreversible event. The Go implementation ships alongside the PHP one and traffic moves behind a flag: shadow first, where the new path runs and its output is compared but not used, then a small share of real traffic, then the rest. The baseline metrics collected before any code changes are what tell you whether the new path is genuinely better rather than just newer. Every step keeps the PHP path deployable, so a rollback is a flag change instead of a redeploy. For queue consumers this is easier than it sounds: both consumers run against the same queue and you shift the share of messages each one takes.
Yes, and Symfony is usually the easier case. The framework pushes teams toward explicit boundaries such as dependency injection configuration, Messenger transports and console commands, and those map cleanly onto workloads that can be lifted out. Messenger handlers are frequently the first thing worth moving, because the message contract already exists and only the handler changes. Doctrine mappings are also useful as documentation of the schema when the domain model is not written down anywhere else.
Yes. Laravel systems tend to have more behaviour wired through framework conveniences such as facades, Eloquent models carrying domain logic, and queued jobs that reach into the container, so the first step is mapping what a job actually touches before deciding whether it can leave. Horizon queues and scheduled commands are the common extraction candidates. Where the boundary turns out to be unclear, it is often cheaper to restructure inside Laravel first and revisit the migration question afterwards.
Discovery has a fixed price: the migration readiness audit is €1,200 and produces an architecture review, a PHP versus Go decision matrix, a risk map and a proposed sequence. Implementation is €550 per day and is scoped after that audit, because before discovery any number would be invented. I do not quote a fixed price for rewriting a monolith, because fixed-price rewrites are how both sides end up arguing about scope instead of doing engineering. Most engagements start with a single workload, which is a small enough commitment that you can stop there if the results do not justify continuing.
That is the intended arrangement. Your engineers hold the expensive knowledge, which is the business rules and the failure history of the system, while what I add is migration experience and the capacity to do the extraction without stalling your roadmap. In practice that means working in your repository, your review process and your tracker, pairing with whoever will own the Go component afterwards, and writing the decisions down so the pattern can be repeated without me. An engagement that leaves your team unable to maintain the result has failed, even if the code works.
Next step
The PHP and framework versions, roughly what the traffic looks like, and the part of the system causing the most pain right now. What you get back is an assessment of whether this is a modernization problem or a migration problem, not a pitch for a rewrite.