Vivian Voss

The Server Question

architecture performance rust

Stack Patterns ■ Episode 06

Seventy-Two Per Cent

72% of the web runs on PHP. This is not an accident. Upload a file, it executes. No build step. No compilation. No deployment pipeline. WordPress powers 43% of all websites. Laravel made it elegant. PHP 8.4 ships enums, readonly properties, and a JIT compiler. For request-response work, PHP is rather good at its job.

It also carries thirty years of luggage. Type coercion that surprises even its advocates. A security history that required rewriting entire APIs from scratch. Legacy it cannot retire without breaking the web it runs on.

But the more interesting observation is architectural. The web has moved. Persistent connections. Live data pushes. Minimal memory footprints. PHP was not built for this. Not a criticism. A design decision, made in 1995, when "the server" meant "generate HTML and close the connection."

The Architecture

PHP's share-nothing architecture is one of the cleanest ideas in web development: every request starts from scratch. No shared state. No leaked context. No previous request contaminating the next one. Beautiful for isolation. Structural impediment for anything that requires keeping a connection open.

PHP: Share-Nothing Architecture Every request starts from scratch. Every response ends in oblivion. Request 1 Worker A Response Request 2 Worker B Response Request 3 Worker A (new) Response Worker A has no memory of Request 1 when Request 3 arrives. 16 GB server / 128 MB per worker = 96 concurrent workers maximum. Persistent connection? The worker dies after response. Like fixing a dripping tap by replacing the sink every hour.

Async PHP exists. Swoole, ReactPHP, Revolt. They are real engineering efforts and they work. They also fight the language's fundamental design: PHP never returns freed memory to the operating system. Long-running processes leak. The workaround is to restart worker processes periodically, which is rather like fixing a dripping tap by replacing the sink every hour.

Five Languages, One Task

The task is simple in specification and revealing in execution: hold persistent connections and push live data. WebSocket, Server-Sent Events, long-polling. The kind of workload that keeps connections open for minutes or hours rather than milliseconds. Five languages. Same task. Rather different outcomes.

Memory per Instance (Persistent Connections) Same task: hold connections open, push live data. Java 256 MB Spring Boot + JVM PHP 128 MB Swoole / ReactPHP Python 60 MB GIL serialises CPU Go 20 MB Goroutines, GC pauses Rust 5 MB No GC, no runtime Three eliminated by overhead. Two remain. Go 15 MB binary, GC pauses Rust 2 MB binary, no pauses 256 MB vs 5 MB. Not a rounding error. A 50x difference.

Java brings Spring Boot and the JVM. Approximately 200 MB before the first byte of application code runs. Deployed before the deployment finishes. PHP needs Swoole or ReactPHP to hold connections at all, and each worker process carries 40 to 128 MB of memory overhead. On a 16 GB server, that is 96 concurrent workers. Python brings elegance and the GIL, which serialises all CPU-bound work onto a single thread. Elegant until load disagrees.

Three eliminated by overhead. Two remain.

The Fair Fight

Go versus Rust. Both compile to a single binary. Both handle ten thousand concurrent connections without visible strain. Both have communities that would rather debate the other's shortcomings than examine their own. The comparison, then, must be structural.

Go assumed garbage collection was free. It is not. Every few milliseconds, the garbage collector pauses to inspect memory, decide what is still alive, and reclaim what is not. For a web server handling short-lived HTTP requests, these pauses are invisible. For a server holding ten thousand persistent connections, every connected client feels every stutter.

Rust has no garbage collector. Memory is freed deterministically when ownership ends. No pauses. No tuning. No prayer-based capacity planning.

Discord: Go → Rust (Production) Same service. Same traffic. Measured in production. Go 0 time GC spikes: 10-40 ms every ~2 min Rust 0 time No GC. No spikes. microseconds "Rust beat Go on every single performance metric." They did not switch back.

Discord ran both in production. Go: latency spikes of 10 to 40 milliseconds every two minutes from garbage collection. Rust: consistent microsecond latency. No spikes. "Rust beat Go on every single performance metric." They did not switch back.

The Larger Question

256 MB per instance versus 2 MB. This is not a rounding error. It is up to a 128-fold difference in resource consumption for identical functionality.

Same 256 MB of Memory Java (Spring Boot + JVM) 1 instance 256 MB consumed Rust 128 instances 2 MB each Every server that need not exist is raw material never mined, water never consumed, energy never spent. Efficient software is not an optimisation. It is resource conservation.

Every server that need not exist is raw material never mined, water never consumed, energy never spent. The industry's answer has reliably been to throw hardware at the problem. Marvellously affordable, until the invoice arrives at both ends: hardware that costs money, and data centres consuming water where none remains.

Efficient software is not an optimisation. It is resource conservation. The difference between one Java instance and 128 Rust instances in the same memory allocation is not academic. It is the difference between scaling by spending and scaling by engineering.

When PHP Is Right

Request-response. Content management. E-commerce. CRUD applications. PHP does these superbly. The 72% chose before the alternatives existed, and for those workloads, they chose correctly. WordPress, Laravel, Symfony: these are not accidents. They are the result of a language that is extremely good at a specific class of problem.

But when the server holds connections open and pushes live data, it is a different workload. Different workload, different tool. One does not criticise a screwdriver for being a poor hammer. One reaches for the hammer.

The Right Tool for the Right Workload PHP (72% of the web) ✓ Request-response ✓ CMS (WordPress, Drupal) ✓ E-commerce (Magento, Shopware) ✓ CRUD applications ✓ Laravel, Symfony Share-nothing is perfect here. Go / Rust ✓ Persistent connections ✓ WebSocket / SSE ✓ Live data push ✓ High concurrency ✓ Minimal memory Keeping connections alive is the job. 72% of the web is not wrong. It runs a different workload.

The Point

72% of the web chose PHP. They were not wrong. They were solving request-response, and PHP does that with the casual competence of a tool that has had thirty years of practice.

But when the question changes from "generate a page" to "hold a connection," the answer changes with it. 256 MB per instance versus 2 MB is not a debate. It is arithmetic. Three languages eliminated by overhead. Two remain. One has a garbage collector that pauses every two minutes. The other does not.

Efficient software is not a luxury. It is responsibility.