The bottleneck is measured.
Profiles, load data or production metrics identify the path that limits the system.
Go alongside PHP
The usual proposal is a rewrite: everything in Go, in twelve months, with the same team that keeps operations running on the side. You know how that ends. There is another way. The one path that buckles under load is carved out and put next to your PHP as a Go service, connected over gRPC or HTTP, on the same database or behind a clear boundary. Everything else stays where it is. And before that comes the measurement: if the bottleneck sits in the database, a different language changes nothing about it, and then I tell you so before you commission anything.
One week, one result, at a fixed price. Putting it live is quoted by scope afterwards, never as an open bill.
Remote from Germany. Straight with me, no agency in between.
The starting point
Almost every grown PHP system has a handful of places that behave differently from the rest: the endpoint the app calls on every start. The import that runs for four hours at night. The search that drags everything else down at peak load. The execution model of PHP is entirely adequate for the largest part of an application, but it sits badly with long-lived connections, with work that should run in parallel, and with calls that have to be answered in single-digit milliseconds.
When that becomes visible, a proposal is usually already on the table: rebuild it, this time in Go. That is a decision about twelve months in which nobody delivers anything else and the old system still has to be maintained. The narrower route is called strangler fig and is not a new idea: you carve out exactly that one path, put it alongside as a service of its own, redirect traffic step by step and leave everything else untouched. No cutover date, no big bang, and a way back at every point.
And then comes the half that appears in no proposal: very often this is not worth doing. The most common reason for a slow path is not the language but a query without a fitting index, an N+1 in the ORM, a database running at its limit, or a third-party service being waited on. In all four cases a Go service waits exactly as long as the PHP in front of it did, only with more moving parts. That is why the measurement comes before the build here. If it points at the database, I say so and we stop there.
Does any of this sound familiar?Who this is for
For CTOs and backend leads with a productive PHP application. One bounded path hits limits in load, concurrency or latency while the rest of the monolith remains useful.
Profiles, load data or production metrics identify the path that limits the system.
Go takes only the workload where an additional language creates a measurable advantage.
Deployment, monitoring, interfaces and knowledge transfer matter as much as the Go code.
What I do
A load test against the suspected path and a trace showing where the time goes: database, serialization, network and interpreter shown separately. The result is a breakdown in milliseconds and a sentence on whether a service of its own would change anything here. If that sentence comes out negative, it still goes in the report, and the package ends there.
What gets carved out is the smallest piece that can stand on its own, with a clearly named way in and way out. Not a module, not a domain, not "the ordering process", but the one call that carries the load. The tighter the cut, the shorter the way back.
A service, not an ecosystem: one binary, few dependencies, concurrency where it earns its place, and a memory footprint you can predict. Plus tests that reproduce the existing path, so the replacement is verifiable rather than merely plausible. It is built in a branch of its own and measured there against real data.
gRPC with protocol buffers where the calls are frequent and a contract between both sides helps. That does assume a long-running PHP process, RoadRunner or FrankenPHP for instance, or calls that tolerate setting up the connection on every request: under classic PHP-FPM the process is short-lived, the gRPC channel does not survive the request, and reusing the HTTP/2 connection is precisely where the gain of gRPC sits. There plain REST with keep-alive is the more honest route, as it is wherever your team gets on better with it. Plus timeouts, retries and a fallback onto the old path, so that a service being down is not a system being down. That part belongs to the route to production, which is written down during this week.
Either the same data with clearly governed access, or a boundary of its own with a defined path for the data. Both are fine, the middle ground is not: two systems writing the same tables as each sees fit is the part that hurts later.
Traffic is redirected behind a feature flag, a fraction first, then more, and the way back is a setting rather than a deployment. The service runs where you already run things, described as infrastructure in code, with the same metrics as the PHP beside it. This step is written down at the end of the week, with an effort estimate, and carried out afterwards as an offer of its own.
Do you have a path that buckles under load?
Send me the path and whatever numbers you have. You get an assessment of whether a service of its own would change anything here, before you commission anything.
How it runs
Four steps within a week. After the second you know whether the third is worth it, and that answer is allowed to be no.
Which path, which load, which numbers you already have. Often a suspicion is on the table by the end of it that has nothing to do with the language. If I have it, I say it right there and not at the end of the week.
A load test and tracing against the path, in a test environment with real data volumes or against a load profile from your production. Afterwards it is settled in milliseconds where the time is spent, split by database, serialization, network and interpreter. If the result points at the database or at a single query, it ends here and you keep the findings.
The service, the interface and the boundary to the data, running in a branch of its own and measured against real data. Before and after on the same operation. Nothing in your production environment is touched along the way, and that is precisely why the fixed price holds.
The connection, the switch-over behind a feature flag, the way back and the running of it, in writing and with an effort estimate. That is where the package ends. Whether you walk that route with your team or with me is yours to decide afterwards; from me you get an offer of its own for it, at a fixed price, before it starts.
The other side
The total is not printed below, because I do not know it. The items I do know. A system that buckles under load does not cost once, it keeps costing every month. Do the maths yourself, then the number is yours.
Take the first item on this list and scale it to a year. If that comes to more than the week below costs, the question of whether to measure is already answered. It replaces none of these figures, but it makes the first of them evidenced, and in case of doubt it spares you the last item on this list.
Entry offer
A fixed price, and it holds because the result is a measurement and a branch in your repository. Nothing in your production environment is touched along the way. Putting it live is quoted afterwards, at a fixed price as well, before it starts.
Not a prototype and not a rewrite but one week with a result: you know in milliseconds where the time in the hot path is spent, whether a Go service changes anything about it at all, and if it does, the carved-out path sits runnable in a branch of its own, measured against real data.
What you get
What you do not get
The outcome
After the measurement it is settled in milliseconds where the time in the hot path is spent, split by database, serialization, network and interpreter. That answers the question of language rather than deferring it. If the answer is yes, the carved-out path sits alongside and the difference is a number in your before-and-after test, not a percentage promise on this page.
Your PHP is not replaced, not rewritten and not frozen. Your team keeps working where it knows its way around and does not have to learn a second language on the side to ship a bug fix.
The way back is part of the plan before the first traffic is redirected: as long as the feature flag stands and the old path runs, taking it back is a setting. That is the real difference to a rewrite. There the way back is gone after the third month, and nobody notices the moment it happens.
The cut, the measurement track and the connection to PHP are thought through once. The next path is then a decision about the cut and not a new project.
Technologies I use
From practice
A payment API for millions of users that had stopped scaling. The existing backend was stabilized first, the new Go services ran alongside and were connected over gRPC. Traffic was redirected step by step rather than switched on a date, without interrupting live payments.
Read the case studyAlso from real projects
How new Go services get added to existing PHP systems without rebuilding everything: strangler fig in practice instead of a big-bang rewrite. The field report behind this service.
Read the article
Who you are talking to
I am Tim Rutte. More than 20 years in software development, many of them in grown PHP systems where I placed Go services alongside without stopping operations. You talk to the person who touches your code, from the first call to the handover.
Common questions
No, and that is the point. Your PHP stays the system where everyday work happens. Alongside it sits a service that does exactly one thing, namely the one the execution model of PHP suits badly. If that service disappears again tomorrow, the old path keeps running for as long as the feature flag stands.
Usually not, and that is the most common case. If the time is spent in a query without a fitting index, in an N+1 in the ORM, in a database at its limit or in waiting for a third-party service, then a Go service waits exactly as long as the PHP in front of it did, only with more moving parts. Without a measurement the question of language is a coin toss. That is why it comes last here and the measurement comes first.
When the time is spent in your own code rather than in waiting: many concurrent, long-lived connections; work that could run in parallel but does not in a request-per-process model; serialization or computation in volume; a call that has to be answered in single-digit milliseconds. You see that in the trace, not in the discussion.
Because the two belong together, and the PHP route is usually the cheaper one. A properly configured OPcache, preloading and above all tidied-up queries get more out of most paths than a second service does. The JIT deliberately does not belong in that list: it helps where something is genuinely computed, and that is rarely the bottleneck, because typical web applications hang on input and output rather than on the CPU. On top of that, the execution model itself can be changed without changing the language: RoadRunner, FrankenPHP and Swoole keep the PHP process alive and address exactly the three weaknesses named above, namely long-lived connections, concurrency and calls that have to be answered in single-digit milliseconds. And anyone still on PHP 7 or older has the upgrade as the first lever anyway, which has a page of its own here. Go only earns its place where none of that is enough. If your case falls into one of the groups before that, I say so and you can spare yourself this page.
Your team has a say in that, and so does your runtime. gRPC with protocol buffers is the better choice where the calls are frequent, a binding contract between both sides helps, and a long-running process sits on the PHP side, RoadRunner or FrankenPHP for instance. Under classic PHP-FPM the process is short-lived, the channel does not survive the request, and that removes the actual gain of gRPC, namely the reused HTTP/2 connection. There, and wherever your people are more familiar with REST, plain REST with keep-alive is the more honest choice. A tool nobody understands in operation is not progress.
Either that, with clearly governed access, or a boundary of its own with a defined path for the data. Both work. What does not work is the middle ground: two systems writing the same tables as each sees fit. Which of the two it becomes belongs in the cut and is settled before anything is built.
That is decided by the cut of your problem and not by the age of the code. This page is about exactly one path that buckles under load while the rest does its job. If the estate itself is the problem, because nobody wants to touch it any more, because there are no tests and every change hurts in three places, then legacy modernization is the better entry point, and then I say so. It has a page of its own here under the services.
Both entry offers cost 1,900 euros fixed, take a week, deliver a branch and are evidenced by the same case study. The only question is what breaks at your end. If it is the tenancy model, the billing or the usage metering, meaning the cut of the platform itself, then the page on SaaS platforms is the right one. If it is not the tenancy model that breaks but a single path, while the rest of the platform carries, you are in the right place here.
That question comes up before the go-live rather than after it, and that is the advantage of the small cut: after the week the service sits in a branch, the route into operation sits beside it in writing, and only then do you decide. It is also why it stays a service and not an ecosystem: one binary, few dependencies, the same metrics and the same delivery as the rest. If your team says it does not want to run it, you have discarded a branch rather than the twelfth month of a rewrite.
The measurement with the carved-out path costs 1,900 euros net and the result is there after a week. That is a fixed price rather than a starting price, and the reason sits in the cut: what is produced in that week is a measurement and a branch in your repository. Nothing in your production environment is touched, so the effort does not hang on other people's approvals, environments and dates either. Not included is the go-live, meaning the connection, the switch-over of traffic and the running of it. That is quoted after the result, at a fixed price as well, before it starts, because only then is its scope known. If the measurement shows a Go service is not the instrument, it ends there. What you have paid for is one week and an answer that spares you a twelve-month project, and that is the cheapest outcome of them all.
Other services
Backends for SaaS and platforms that hold under real load: Go and PHP 8, event-driven, with tenant isolation and recovery designed in.
Learn moreFrom a pipeline nobody wants to touch to deployments on a Tuesday morning: one artefact through every stage, access without long-lived keys, a rehearsed way back.
Learn moreSaaS platforms that still hold at the tenth customer: a decided tenant model, billing with usage metering you can prove, and a list of what breaks first at ten times the volume.
Learn more