The PHP Runtime
Classic PHP is built the way almost no other server-side language is: every request starts from a clean slate. The process boots the application, reads the config, assembles the container, handles the request, returns the response — and destroys everything: variables, objects, static properties, globals. Nothing carries into the next request. This is what shared-nothing means, and it is simultaneously PHP's greatest safety net and its greatest tax. A safety net, because a memory leak lives for milliseconds and never accumulates, and a crashed request cannot corrupt its neighbour. A tax, because you pay for a full application bootstrap on every single request.
Two mechanisms grew out of that tax, and the whole topic revolves around them. The first is OPcache: it keeps already-compiled opcodes in shared memory so the source is not re-parsed on every request. And here is the topic's headline trap: OPcache is a code cache, not a data cache; confusing it with Redis or Memcached is the single most common wrong answer, and it is what candidates get filtered on. The second mechanism is the worker runtimes (FrankenPHP, RoadRunner, Laravel Octane): they abandon shared-nothing and keep the loaded application in memory between requests. The bootstrap disappears and database connections are reused — but so do statics, singletons and everything you forgot to reset carry into the next request. That trade — speed against leaking state — is the spine of this topic. On top of it sits the memory model — copy-on-write, refcounting and the cycle collector — and the lazy generators that let you walk a million rows without ever assembling them into an array. The layer-by-layer breakdown is below.
Topic map
- The process model — shared-nothing per request, what survives the request boundary and what does not, and how a long-lived CLI daemon differs.
- php-fpm and nginx — the FastCGI worker pool, where the concurrency ceiling comes from, and how to size
pm.max_children. - OPcache and JIT — a cache of opcodes, not of data; the production settings,
opcache.validate_timestamps, and why JIT barely helps an ordinary web request. - Worker mode — FrankenPHP, RoadRunner and Octane keep the application in memory; what that buys and what class of bug it brings.
- Zvals and copy-on-write — why passing a huge array by value is free right up until the first write to it.
- Garbage collection — refcounting frees almost everything, but never a cycle; what the cycle collector does and how a leak differs from retention.
- Iterators — the
Iteratorinterface, theforeachprotocol, andIteratorAggregate. - Generators —
yield, lazy one-value-at-a-time production, andyield fromdelegation. - Modern PHP — what PHP 8.0–8.5 brought, what
strict_typesdoes, and what concurrency in PHP is actually built on.
Common Mistakes and Traps
| Mistake | Consequence |
|---|---|
| Treating OPcache as a data cache, "like Redis" | It caches opcodes; query results and HTML are not in it and never will be |
| Expecting JIT to speed up an ordinary web request | A typical request is bound by the database and the network; JIT accelerates CPU-bound code, not waiting on I/O |
| Believing nginx executes PHP itself | It serves the static files and delegates PHP to a php-fpm worker over FastCGI |
| Believing php-fpm forks a process per request | Pool workers are reused; the concurrency ceiling is pm.max_children |
Raising pm.max_children "to handle the load" | The workers push memory into swap, which is worse than queueing in the backlog |
Leaving opcache.validate_timestamps=0 and just copying new code up | The workers keep executing the old opcodes — a reset or an FPM reload is required |
| Moving an application into worker mode without purging its state | Singletons and statics carry into the next request — one user's data in another's session |
| Expecting worker mode to help when bootstrap is 15 ms out of 90 | The ceiling on the gain is ≈17%; the rest is database and network, which worker mode does not touch |
| Believing that passing a big array by value copies it | There is no copy — only a refcount bump; the copy happens on the first write while refcount > 1 |
| Expecting refcounting to free everything | A reference cycle never drops to zero — only the cycle collector frees it |
Curing CLI memory growth with gc_collect_cycles() | If live references to the objects exist, this is retention, not garbage — the collector is powerless |
| Believing a generator builds the collection up front | The whole point of yield is the opposite — values are produced one at a time and memory stays flat |
| Trying to walk a generator twice | A generator is one-way: a second traversal throws |
Treating strict_types=1 as global | It applies only in the file that declares it, and it follows the caller's mode |
Interview relevance
The runtime is the section where interviewers check whether the candidate has a model of what actually happens to a request rather than a recital of the docs. A good answer to "how does PHP work under load" sounds like this: "a free worker in the php-fpm pool accepts the FastCGI request, the opcodes are already in OPcache, the application boots from scratch, returns the response and destroys everything; concurrency is capped by pm.max_children." That candidate is immediately apart from the one who says "nginx runs the script".
Typical checks:
- What shared-nothing means and what exactly survives the request boundary.
- The roles of nginx and php-fpm, and where the ceiling on concurrent requests comes from.
- What OPcache stores and what it is not; what
opcache.validate_timestamps=0does and at what price. - How JIT differs from OPcache and why it gives an ordinary web request almost nothing.
- What a worker runtime reuses between requests and what class of bug that introduces.
- What happens in memory when a large array is passed by value.
- What refcounting cannot free, and what the cycle collector does about it.
- How a generator differs from an iterator and what
yield fromadds.
Common wrong answer: "OPcache is a cache, it stores results so you do not have to hit the database." This is the most frequent and most revealing misconception of the topic: OPcache knows nothing about your data or your queries — it stores compiled code, so that the engine does not re-parse .php files on every request. Caching data is what Redis and Memcached are for, and that is an entirely different layer. The second classic failure is "we will move to Octane and everything gets several times faster": if bootstrap is 15 ms out of 90, the ceiling on the gain is about 17%, and the price is a whole new class of incidents involving state leaking between requests.