Tags: web-dev concept

The Request Lifecycle in PHP

Date: 2026-09-27


Classic PHP is shared-nothing: every request starts from an empty interpreter and ends by throwing everything away. That one fact explains PHP’s robustness, its per-request bootstrap cost, why it needs an external connection pooler and a cache, and exactly which bugs appear when you switch to a long-running worker.


The PHP request lifecycle is the sequence a request goes through — a process picks it up, the script’s state is built from nothing, a response is produced, and all request state is destroyed. Shared-nothing is the property that follows: no request can see anything another request left in memory.

Who does what

browser
   │
   ▼
web server (nginx)        static files served directly; .php forwarded
   │  FastCGI
   ▼
PHP-FPM master            manages a pool of worker processes
   │
   ├─ worker 1 ── one request at a time ──┐
   ├─ worker 2                            │
   └─ worker N                            │
                                          ▼
                  ┌────────────────────────────────────────────┐
                  │ per request                                │
                  │  1 init       superglobals built:          │
                  │               $_GET $_POST $_COOKIE $_SERVER│
                  │  2 compile    source → opcodes              │
                  │               (skipped if OPcache has them) │
                  │  3 execute    your code; framework boots   │
                  │  4 output     headers + body to FastCGI     │
                  │  5 shutdown   every variable, object,       │
                  │               connection freed              │
                  └────────────────────────────────────────────┘
                   worker survives, state doesn't → next request

PHP-FPM (FastCGI Process Manager) is the usual runtime: a master process managing a pool of workers, each handling exactly one request at a time. FastCGI is the protocol the web server uses to hand it requests. The worker process is reused; the interpreter state is wiped between requests.

Concurrency is process count. Twenty workers means twenty simultaneous requests; the twenty-first waits in the queue. There’s no event loop, so a request blocked on a slow API holds a whole worker for the duration — vs JS: Node would serve other requests during that wait — The Event Loop · Async Models.

What survives, and what doesn’t

DIES WITH THE REQUEST                  SURVIVES
local and global variables             OPcache — compiled opcodes, shared memory
static properties, singletons          APCu — a key-value cache in shared memory,
database connections (unless             per server, per FPM pool
  persistent)                          anything external: Redis, the database,
open files, sessions in memory           the filesystem
the framework's booted container       the worker process itself

OPcache is why the model is affordable. Without it every request re-parses every file; with it, compiled code is read from shared memory. It’s the first thing to confirm is on in production. Preloading (PHP 7.4+) goes further, loading chosen classes into memory once at server start.

Everything else must be rebuilt. A framework like Laravel or Symfony constructs its service container, reads config and registers routes on every request — the cost that framework config and route caching exist to cut.

Consequences

Good ones:

  • Leaks are self-healing. A memory leak lasts one request. pm.max_requests recycles a worker after N requests anyway, as insurance against leaks in extensions
  • Crashes are contained. A fatal error kills one response, not the server
  • No data races between requests. Nothing is shared in memory, so there’s nothing to lock — Race Conditions still happen, but in the database, not the process

Costly ones:

  • No in-process connection pool. Every request opens and closes its database connection; at scale, an external pooler (PgBouncer for Postgres, ProxySQL for MySQL) sits in between — Reference - PHP covers the persistent-connection option and why it’s risky
  • No in-memory cache between requests. Caching means APCu or Redis — Caching Strategies
  • No work after the response, natively. Sending an email means making the user wait, or a queue plus a separate worker process — Message Queues. Under FPM, fastcgi_finish_request() flushes the response and lets the script carry on, which is a hack for small jobs, not a queue
  • Timeouts are per request. max_execution_time and the web server’s FastCGI timeout both cap a request; long imports belong in a CLI (command-line) process, which has no time limit by default

Long-running PHP: the model turned off

Worker-mode runtimes — FrankenPHP’s worker mode, RoadRunner, Swoole, with Laravel Octane as a common wrapper — boot the application once and keep it in memory, feeding it requests in a loop. The bootstrap cost disappears. So does shared-nothing.

final class CurrentCustomer
{
    private static ?Customer $customer = null;
 
    public static function set(Customer $c): void { self::$customer = $c; }
    public static function get(): ?Customer { return self::$customer; }
}
 
// Request 1, logged in as ana:  CurrentCustomer::set($ana);
// Request 2, anonymous visitor: CurrentCustomer::get()
//   → FPM:          null. The static died with request 1
//   → worker mode:  $ana. The static survived — ben now sees ana's basket

Code written for FPM was allowed to be careless about statics and singletons, because shutdown cleaned up after it. In a worker that same code leaks memory steadily and, worse, leaks one user’s data into another’s response. Frameworks reset their own state between requests (Symfony via ResetInterface, Octane via its own resets); your services and any package holding static state are your problem.

Before adopting a worker runtime, audit statics and singletons, set a max-requests recycle as a safety net, and load-test with interleaved logged-in users rather than one — Memory and Long Sessions is the browser-side version of the same leak shape. [CHECK: current worker-mode support and reset behaviour per framework and runtime — this area is moving.]

vs Node, in one table

PHP-FPMNode
Process lifetimePer request (state), reused (process)One process, whole uptime
ConcurrencyOne request per workerMany per process, via the event loop
Blocking I/ONormal and fineStalls every other request
Module-level stateGone after each requestPersists, shared by all requests
Memory leakLasts one requestGrows until restart
Connection poolExternalIn-process