Skip to content

PHP runtime, memory, OPcache, and process models

Status: Complete. Last reviewed 2026-08-28.

PHP performance and state lifetime depend on both the Zend Engine and the SAPI/process model hosting it. Source becomes opcodes, values live in engine containers and shared structures, and a process may serve one command, many isolated requests, or many jobs inside one booted application. “PHP starts fresh” is therefore a deployment-specific statement, not a language guarantee.

The engine lexes and parses PHP source, compiles it into opcodes, and executes those opcodes in a virtual machine. OPcache stores compiled script bytecode and related interned data in shared memory, allowing later requests to avoid repeated parsing and compilation. It does not cache database results, HTTP responses, rendered pages, or arbitrary application values.

OPcache must be sized and deployed deliberately. A cache too small for the script set churns or fills; timestamp validation policy determines how changed files are discovered. Immutable release directories plus process reload/invalidation avoid mixed code. Disabling timestamp checks without an explicit reset/reload plan can leave old opcodes serving new files.

Preloading executes a configured script at server startup and can keep selected functions/classes available across requests. Preloaded definitions are not reloaded casually, so deployment requires restarting the owning process. Preloading is not a substitute for ordinary OPcache and can increase startup and shared-memory complexity for modest gains.

JIT can compile selected opcode paths to machine code. It targets CPU execution cost; it does not remove database latency, network waits, lock contention, framework allocation, or inefficient queries. Typical Laravel requests spend substantial time in I/O and orchestration, so OPcache, query work, caching, and capacity diagnosis commonly matter more.

Enable JIT only after a representative profile shows CPU-bound PHP code that it may improve, then benchmark throughput and tail latency with realistic warmup. Account for JIT buffer memory and operational complexity. Since PHP 8.4 JIT is disabled by default, and startup fails if explicitly enabled JIT initialization fails; deployments should not assume old defaults.

At engine level a variable slot refers to a zval carrying a value and type metadata. Refcounted structures such as strings, arrays, objects, and resources can share underlying allocations. Array/string value semantics permit copy-on-write: assignments can share until mutation requires separation. Object values contain handles to an object store entry, so assignment shares instance identity.

Explicit references introduce aliasing between variable containers. They can affect separation and lifetime, which is why adding & as a memory optimization is unsafe without proving semantics. The language-level rules in the arrays and object chapters are the contract; zval vocabulary explains an implementation mechanism, not permission to depend on undocumented internal layout.

Reference counting releases most unreachable values promptly. Cyclic object/array graphs can remain refcounted even when unreachable from program roots, so PHP’s cycle collector identifies and frees eligible cycles. Collection costs CPU; disabling it or creating unbounded cycles in a worker can trade short-term throughput for long-term memory growth.

memory_get_usage() and memory_get_peak_usage() report memory tracked by PHP’s allocator under their documented modes. Operating-system resident set size includes code pages, shared mappings, extension allocations, allocator arenas, JIT/OPcache mappings, and pages the allocator retains for reuse. Freeing a PHP value therefore need not immediately reduce RSS.

Distinguish three patterns:

  • Per-operation peak: one request/job temporarily builds a large graph and may exceed memory_limit.
  • Retained userland state: reachable references accumulate across jobs in statics, singletons, listeners, caches, closures, or ORM identity graphs.
  • Allocator/extension growth: PHP usage stabilizes while RSS stays high or grows due to reuse, fragmentation, native libraries, or a leak outside ordinary userland tracking.

Measure both PHP and OS/container metrics over repeated representative work. A one-request snapshot cannot diagnose a worker slope.

FPM provides request cleanup inside reused processes

Section titled “FPM provides request cleanup inside reused processes”

PHP-FPM manages worker processes. A worker handles one request at a time, then request-scoped userland memory is torn down before that process can serve another request. OPcache and selected persistent extension/process resources remain outside ordinary request state. The process itself is reused, but typical userland statics do not persist as they do in a never-ending CLI loop.

Pool modes trade idle capacity and startup behavior: static maintains a fixed number, dynamic adjusts within configured bounds, and ondemand creates workers when traffic arrives. pm.max_children limits simultaneous executing requests. Once all workers are busy, requests wait in the listen queue; tail latency rises even if individual PHP execution time is unchanged.

Size a pool from measured per-worker RSS, host/container memory available after other services and OPcache, concurrency targets, and downstream capacity. CPU count alone is insufficient for I/O-heavy work, while setting hundreds of workers can overload the database and cause memory pressure. Observe active/idle workers, queue depth, max-children events, request duration, RSS, and downstream saturation together.

pm.max_requests can recycle workers after a number of requests, limiting damage from extension leaks or fragmentation. It is a containment mechanism, not a substitute for finding reproducible growth.

Long-running CLI, queue, and Octane lifetimes

Section titled “Long-running CLI, queue, and Octane lifetimes”

A CLI command normally owns one process invocation. Queue workers loop across jobs, so loaded code, configuration, container singletons, static properties, registered callbacks, and retained objects can survive. Deployments must restart workers to load new code and configuration, and job boundaries must reset job-scoped context.

Octane boots the Laravel application once per worker and serves multiple requests through it. Request objects and containers are supplied per request, but objects captured in long-lived singletons or static state can leak tenant/user/request data. Injecting a concrete request or config repository into a singleton during boot can freeze stale data. Resolve request-scoped values at use time or use the framework’s scoped lifecycle deliberately.

Worker limits and graceful restarts bound risk. Supervisors should send appropriate signals, allow current work to finish within a grace period, and replace exited processes. Abrupt kill can interrupt any userland cleanup, so jobs still need idempotency and recoverable state.

  1. Define the symptom: latency, throughput, memory limit, RSS, crash/restart, or stale state.
  2. Separate CPU execution, external I/O, lock wait, and queueing with profiles/traces and system metrics.
  3. Reproduce multiple operations in the same process; record PHP usage, peak, RSS, object/listener/cache counts, and tenant/job identity.
  4. Compare FPM request behavior with the actual long-running runtime instead of relying on feature tests that reboot the app.
  5. Inspect OPcache status, restart/invalidation policy, pool queue/max-child signals, and worker deploy lifecycle.
  6. Change one mechanism, then verify the memory slope or latency distribution under representative load.
  • OPcache timestamp validation is disabled, but deploys switch files without reset/reload, serving stale code.
  • FPM max children is exhausted; p50 stays stable while queued requests inflate p99.
  • A queue worker accumulates event listeners or ORM graphs and is masked by frequent recycling.
  • An Octane singleton captures the first tenant’s request context.
  • A team enables JIT for an I/O-bound endpoint and sees no meaningful improvement.
  • PHP memory looks stable while native extension or allocator RSS grows until the container is killed.
  • Current: PHP 8.5 with OPcache is the production baseline; JIT remains workload-specific and disabled by default unless configured. Use immutable deployment/restart discipline.
  • Common: FPM remains widespread, while Laravel queue workers and Octane make long-lived-state reasoning essential in PHP 8.2–8.4 estates.
  • Legacy: mod_php, mutable in-place releases, ad hoc daemon loops, and folklore-based worker sizing require measurement before migration. Preserve the actual SAPI/process assumptions in runbooks.