PHP → Go

PHP vs Go for Backend Systems: Choose Based on the Workload

A workload-by-workload comparison of PHP and Go, runtime model, memory, concurrency, productivity and operations, without pretending either language wins everywhere.

Most PHP vs Go backend debates are really debates about taste, and they produce nothing useful. The two languages are not competing for the same job description. PHP is a request-scoped language with an enormous framework ecosystem built around rendering and CRUD. Go is a compiled language with a concurrency model designed for long-lived network services. Asking which is “better” is like asking whether a van is better than a motorcycle. The honest answer depends entirely on what you are moving and how far.

So this comparison is organised by workload, not by language feature. For each dimension, the question is the same: does the difference between PHP and Go change anything for your system, or is it a detail you will never feel?

The runtime model is the root of every other difference

Classic PHP runs one request per process. PHP-FPM keeps a pool of worker processes; a request arrives, a worker picks it up, the framework bootstraps, the request is served, and the worker’s memory is reset. Nothing survives between requests except what you deliberately put in a cache or a database. This is a genuinely good design: memory leaks are self-healing, a crash affects one request, and reasoning about state is trivial because there is almost none.

Go runs one process that handles many requests concurrently. Each request gets a goroutine, a lightweight, runtime-scheduled unit of execution starting with a small stack that grows as needed. The scheduler multiplexes goroutines onto OS threads, and when a goroutine blocks on network I/O the scheduler parks it and runs another. State lives in the process for as long as the process runs.

Everything else on this page follows from that difference.

Dimension PHP (FPM, 8.x) Go What it means for you
Concurrency unit OS process per request Goroutine, scheduled in-process Go holds far more concurrent connections per machine
Memory Full interpreter + bootstrap per worker Shared heap, small per-goroutine stacks PHP fleet size is often set by RAM, not CPU
Long-lived connections Awkward without Swoole / RoadRunner Native WebSockets and SSE are a Go strength
Shared state Reset each request Persists in process Go needs care with races; PHP rarely does
DB connections Roughly one per worker Bounded pool per process PHP fleets often need PgBouncer
Startup cost Bootstrap per request (opcache helps) Once, at process start Matters most at high request rates
Type safety Gradual, runtime-checked Static, compile-time Go catches a class of bugs before deploy
Deployment artefact Source + runtime + extensions Single static binary Simpler images, no extension drift
Framework support Very deep (Laravel, Symfony) Deliberately thin PHP is faster for CRUD and admin UIs
Hiring pool (EU) Large Smaller but growing Affects maintenance risk more than build speed

Keep that table in mind rather than memorising it; the sections below say what each row actually costs.

Request/response web applications

For a typical B2B web application, forms, listings, dashboards, an admin area, moderate traffic, PHP is a strong choice and often the better one. Modern PHP 8.x is a genuinely good language. Typed properties, enums, readonly classes, constructor promotion, match expressions, fibers, and attributes have closed most of the gaps that made PHP unpleasant a decade ago. Opcache eliminates recompilation, and the JIT helps CPU-heavy loops even if it does little for I/O-bound request handling.

More importantly, Laravel and Symfony bring things you would otherwise build by hand in Go: validation, form handling, an ORM with migrations, authentication and authorization scaffolding, mail, queues, an admin ecosystem, and a large body of maintained packages. For an application whose bottleneck is how fast the team can express new business rules, that matters more than nanoseconds.

Go wins this category only when the endpoint is high-volume, latency-sensitive, and thin, public read APIs, edge services, token validation, feed endpoints. There, the absence of per-request bootstrap and the flat tail latency under concurrency are real.

Concurrency

This is where the two are not comparable.

In PHP, concurrency is horizontal: more processes, more machines. Within a request, work is sequential unless you reach for curl multi-handles, fibers, or an async runtime like Amp or ReactPHP. Those work, but they sit outside the mainstream of most codebases and most libraries block.

In Go, concurrency is the default idiom:

g, ctx := errgroup.WithContext(ctx)
for _, id := range supplierIDs {
    id := id
    g.Go(func() error {
        return fetchQuote(ctx, id)  // runs in parallel
    })
}
err := g.Wait()

If your request handler needs to talk to six upstream services, PHP takes the sum of the latencies and Go takes roughly the maximum. For an aggregation endpoint calling several partner APIs, that is the difference between an interface that feels sluggish and one that does not.

The flip side: goroutines introduce data races, and shared mutable state in a long-lived process is a class of bug PHP developers rarely encounter. go test -race is not optional.

Workers and background jobs

Background processing is where the runtime difference shows up in the invoice.

A PHP worker process typically holds the full framework in memory for the lifetime of the job. Because long-running PHP processes can accumulate state, the standard practice is to restart workers periodically, --max-jobs, --max-time, or a supervisor policy. Suppose, illustratively, each worker sits at 150 MB and your jobs are 90 percent network wait: running 60 concurrent jobs costs you roughly 9 GB of memory to keep 60 sockets busy.

A Go consumer does the same work with a bounded worker pool inside one process. Concurrency is a number you set, memory is dominated by whatever the jobs actually allocate, and the same process can hold a single bounded database pool rather than 60 separate connections.

That is why queue consumers are the most common first Go service in a PHP shop, and why the migration is comparatively safe: the contract is a message, not an HTTP response. The practical mechanics, idempotency keys, at-least-once delivery, draining and replaying during cutover, running both consumers against the same queue, are covered in moving PHP queue workers to Go.

Memory and infrastructure cost

PHP fleets are usually sized by memory, not CPU. You compute pm.max_children from available RAM divided by peak per-worker memory, and that number sets your concurrency ceiling. Under a traffic spike, requests queue behind a full worker pool and latency climbs sharply once saturation hits.

Go services are usually sized by CPU and by whatever their caches hold. They degrade more gracefully under load, because an extra concurrent request costs a goroutine stack rather than a process.

The caveat is that Go has a garbage collector, and allocation-heavy code produces GC pressure that shows up in tail latency. It is tunable with GOGC and GOMEMLIMIT, and it is a smaller problem than process-per-request memory, but “Go has no memory concerns” is wrong.

Deployment and operations

Go’s deployment story is simply simpler. go build produces a static binary. Your container image can be a scratch or distroless base with one file in it. There is no runtime version to match, no extension matrix, no composer install at deploy time, no opcache to warm, no FPM pool to tune. Cross-compilation to the target platform is one environment variable. Health checks, graceful shutdown, and signal handling are straightforward with context and http.Server.Shutdown.

PHP deployment is well understood but has more moving parts: PHP version, extension versions, opcache configuration, FPM pool sizing, and the interaction between them. Containers have improved this considerably, and a well-run PHP platform is stable, it is just more configuration surface.

On observability the two are close. Both have solid OpenTelemetry support. Go has an advantage in built-in runtime profiling: net/http/pprof gives you CPU, heap, goroutine, and mutex profiles from a live production process with almost no setup. PHP has good profilers too, but they are usually a separate extension and a separate decision.

Developer productivity

Be honest here, because it is where migrations quietly lose money.

PHP with a mature framework is extremely productive for the kind of work most business applications consist of. Scaffolding a resource with validation, persistence, an admin screen, and a queued side effect is minutes of work. The ecosystem has a package for nearly everything, and the documentation and tutorial culture is excellent.

Go is productive for services and deliberately unhelpful for applications. There is no standard ORM, no standard router until recently, no scaffolding, and error handling is explicit at every call site. That verbosity is a genuine reliability feature in a payments consumer and a genuine annoyance in a CRUD module. Compilation, static types, a fast test runner, gofmt, and a small language spec make Go code easy to read six months later and easy to onboard onto, but the first three months for a PHP team are slower, not faster.

A reasonable heuristic: if the code will change weekly for business reasons, favour PHP. If the code will change quarterly and run constantly, favour Go.

Ecosystem and team knowledge

Your team’s existing knowledge is a real engineering input, not a soft factor. A team of six PHP engineers with one person who has shipped Go is not a team that should be running five Go services. Either invest properly in that skill, pairing, code review standards, a written set of conventions for project layout, error wrapping, context propagation, and testing, or keep the Go surface small enough that one or two people can own it.

The hiring picture in Europe is worth thinking about honestly. PHP has a deep pool in most markets. Go’s pool is smaller but skews senior, and Go developers are typically comfortable operating what they build. The risk to plan for is not hiring; it is the bus factor on a Go service inside a PHP organisation.

Migration cost is a first-class input

The comparison table above says nothing about the cost of getting from where you are to the other column. That cost is usually the deciding factor.

Rewriting working code has a negative expected value on day one: the new version is less tested, its edge cases are unknown, and the business rules encoded in the old version were learned expensively. You take that hit deliberately when the execution model is the constraint. You should refuse to take it when the constraint is a missing index, an N+1 query, an unbounded result set, or a cache you never added. This is the reasoning behind how I scope selective PHP to Go extraction work: the deliverable is a decision about which workloads justify the cost, not a fixed quantity of rewriting.

I would put it this way: measure before you migrate. Run a sampling profiler in production, read the actual query plans, check opcache hit ratio and memory sizing, look at whether time is spent in PHP or waiting on something else. The approach is laid out in what to fix in PHP before considering a rewrite, and it frequently ends the conversation, tuned PHP hits the target for a fraction of a migration budget.

A decision matrix

If the workload is… Choose Reason
Admin panel, back office PHP Framework scaffolding does the work
CRUD with weekly rule changes PHP Change speed beats runtime cost
Server-rendered pages PHP Mature templating, mature caching
Queue consumers, high I/O wait Go Concurrency without process memory
WebSocket / SSE gateway Go Persistent connections per process
Fan-out aggregation endpoint Go Parallel calls, one deadline
CPU-heavy transforms Go Compiled, parallel by default
High-volume thin read API Go Flat tail latency at concurrency
Slow because of the database Neither Fix the query plan first
Slow because of coupling Neither That is architecture, not language

The last two rows are the ones that save the most money.

The question worth asking instead

If you are weighing PHP against Go for a system that already exists, “PHP or Go?” is the wrong framing. Nobody is going to run a single language for everything, and nobody should. The useful question is narrower and more actionable: which specific parts of our PHP platform would materially improve if they were Go, and what would that cost to do safely?

That is a decision that benefits from someone who has no incentive to maximise the size of the rewrite. If you want it answered properly, workloads classified, constraints measured, an extraction sequence with rollback at every step, and a written list of what should stay in PHP, that is what the PHP to Go migration service covers. It is scoped as a fixed-price readiness audit at €2,500, and a legitimate outcome of that audit is “tune the PHP you have, extract nothing yet.”