skip to content

Speed & Footprint

OPcache and the JIT, refcounting and the cycle collector, profiling technique and the APCu user cache. Interviewers probe where a PHP request really spends its time and memory.

part ofPHPoverview, primer and where to startread it →
on this pageshow

explore

questions

22

In PHP, what does the fatal error "Allowed memory size of 134217728 bytes exhausted" mean, and what does memory_limit control?

level: juniorimportance: must knowfreq 55%

answer

  1. 134217728 bytes is 128M
  2. a per-process cap on the engine's allocator
  3. E_ERROR: try/catch cannot stop it
  4. -1 removes the limit
  5. max_memory_limit caps it since 8.5

basics

~20 s

The script asked PHP's memory manager for more than memory_limit allows (134217728 bytes is the default 128M), so PHP stopped with a fatal error that try/catch cannot catch. memory_limit caps one process's engine allocations; -1 removes it.

solid answer

~50 s

`memory_limit` is the most memory one PHP process — one request, or one CLI run — may take from PHP's own memory manager. The default is `128M`, which is exactly 134 217 728 bytes. When an allocation would cross it, PHP raises a fatal `E_ERROR`: "Allowed memory size of … bytes exhausted (tried to allocate N bytes)". It is not an exception, so `try`/`catch` cannot handle it; shutdown functions still run, which is where you can log it. The "tried to allocate" figure is just the last straw, not necessarily the culprit. The setting is changeable at runtime with `ini_set('memory_limit', '512M')`, and `-1` disables it. Since PHP 8.5 a startup-only `max_memory_limit` (default `-1`, no cap) can bound what `memory_limit` may be raised to. Raising the limit is a valid fix for a known heavy job; for anything else I first find what is accumulating.

go deeper

for a junior

Recognise the error, know that 134217728 bytes is the default 128M, and that ini_set('memory_limit', …) or -1 changes the cap.

for a middle

Explain that the limit is per process and measured on PHP's allocator, why try/catch misses it, and how a shutdown function can log it.

for a senior

Treat the error as a symptom: locate the accumulation, prefer streaming, and raise limits only for bounded jobs, using max_memory_limit to keep code from lifting its own cap.

for a principal

Set memory budgets per workload type, align them with container limits, and decide where a hard ceiling protects the fleet better than a generous one.

## What the limit measures PHP allocates almost everything a script uses — strings, arrays, objects, compiled data — through its own **memory manager**, the Zend allocator. `memory_limit` caps how much that allocator may reserve for **one PHP process**: a single web request in PHP-FPM, or the whole run of a CLI script. The check is made against the memory the manager has reserved from the system, the figure `memory_get_usage(true)` reports. | Setting | Default | Scope | |---|---|---| | `memory_limit` | `128M` | changeable anywhere, including `ini_set()` at runtime | | `max_memory_limit` (PHP 8.5+) | `-1` (no cap) | startup only; upper bound for `memory_limit` | `128M` is 128 × 1024 × 1024 = **134 217 728 bytes**, which is why that number is so familiar from error logs. ## Reading the error ``` PHP Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 20480 bytes) ``` - **"Allowed memory size"** is the current `memory_limit` in bytes. - **"tried to allocate"** is the size of the allocation that crossed the line. It is often small: the memory was consumed by everything allocated before it, and this request was merely the last straw. - The file and line point at where that last allocation happened, which is frequently a loop body, not the place that retains the data. ## Why try/catch does not help Memory exhaustion is an `E_ERROR` fatal error, not an `Error` or `Exception` object, so no `catch` block sees it. The script stops. Functions registered with `register_shutdown_function()` still run, and `error_get_last()` inside them returns the fatal error, which is the standard way to log what happened. ## Changing the limit 1. **In php.ini or the pool configuration**, for all scripts of that SAPI. 2. **At runtime**: `ini_set('memory_limit', '512M')` in a script that is known to need more, typically a CLI job. 3. **`-1`** means no limit at all. Useful for a supervised batch job; risky for web requests, where one runaway request can exhaust the server. Two runtime failure cases exist. Setting a value lower than what the process already uses fails with a warning. And since PHP 8.5, if `max_memory_limit` is set, asking for more than it allows produces a warning and the limit is set to `max_memory_limit` instead. That lets an operator stop application code from quietly lifting its own ceiling. ## What is not counted The limit covers memory taken through PHP's allocator. Memory that an extension or C library allocates directly from the system is not counted, and the operating system's view of the process (resident size) is usually larger than PHP's figures. So a process can be killed by a container or OS memory limit while PHP's own numbers look fine, and the reverse is also possible. ## How to respond - **Find what accumulates.** Log `memory_get_usage()` at intervals inside the loop that fails; a steady climb means data is retained per iteration. - **Stream instead of loading.** Read files line by line and query results row by row, instead of pulling everything into one array. - **Release as you go.** Do not keep every processed record in an array "for later" unless you need it. - **Raise the limit deliberately** for jobs whose peak is known and bounded, in that job's own configuration rather than globally. ## Web requests versus CLI jobs - **Web requests** share a server. A generous limit multiplied by many concurrent workers can exceed the machine's memory, so web limits are usually kept modest and a request that needs more is treated as a design problem. - **CLI jobs** often run alone and can justify a higher limit, set in the job itself with `ini_set()` or with `php -d memory_limit=1G script.php`, rather than raised for every script on the host. - **Long-running workers** reach the limit through slow growth over many jobs rather than one large allocation; the per-iteration measurement above is the way to tell the two apart. Whichever the context, the number in the error message is only the configured ceiling. The useful information is the trend that led up to it, so collect `memory_get_usage()` readings before changing any setting. A limit error in a web request usually means the request loads too much data at once; in a CLI job, it usually means something grows with each iteration.

  • How do you log a memory-limit failure if try/catch cannot see it?
    Register a shutdown function with `register_shutdown_function()`. Shutdown functions still run after a fatal error, and inside one `error_get_last()` returns the fatal error's type, message, file and line, which you can send to your log.
  • What does max_memory_limit add in PHP 8.5?
    It is a startup-only ceiling for `memory_limit`, defaulting to `-1` (no ceiling). If code or configuration asks for a higher `memory_limit`, PHP emits a warning and sets the limit to `max_memory_limit` instead, so application code cannot raise its own limit past what the operator allows.

saying these in an interview costs you the question

  • catch (Throwable $e) can recover from memory exhaustion
  • The allocation named in the error is what leaked
  • memory_limit caps the whole server's memory
  • memory_limit cannot be changed at runtime
  • PHP's memory figures always match the process size in the OS
open as a page

In PHP, what does OPcache do for a web application, and what does it not cache?

level: juniorimportance: must knowfreq 70%

basics

~20 s

OPcache stores each script's compiled opcodes in shared memory, so later requests skip reading and compiling the PHP source and run the cached opcodes directly. It caches code only: not query results, not rendered pages, not application data.

open as a page

In PHP, when does caching a feature-flag list in APCu on each web server beat Redis, and when does it give wrong answers?

level: middleimportance: must knowfreq 42%

basics

~20 s

APCu wins for small, read-heavy, rarely changing data such as a flag list: a local memory read, no network hop. It gives wrong answers when servers must agree, because each server holds its own copy and cannot invalidate the others.

open as a page

In PHP, what do opcache.validate_timestamps and opcache.revalidate_freq control, and why do production servers often disable validation?

level: middleimportance: must knowfreq 50%

basics

~20 s

With opcache.validate_timestamps=1 (the default) OPcache checks a cached script's modification time at most every opcache.revalidate_freq seconds (default 2) and recompiles it if it changed. Production often sets 0: no file checks at all, so a deploy must reset OPcache.

open as a page

After a release, a PHP product page is 800 ms slower in production; how do you find the slow path instead of guessing at it?

level: seniorimportance: must knowfreq 45%

basics

~20 s

Confirm and scope the regression, split wall time from CPU time, narrow it with hrtime spans around each phase, then profile production traffic with a low-overhead sampling profiler on a small fraction of requests and compare against the previous release.

open as a page

In PHP, how do APCu's apcu_store() and apcu_fetch() keep data between requests, and why is comparing apcu_fetch()'s result with false not enough?

level: juniorimportance: should knowfreq 36%

basics

~20 s

APCu is a PECL extension that keeps user data in the shared memory of one PHP server, readable by later requests. apcu_fetch() returns false on a miss, so a cached false is indistinguishable unless you pass its by-reference $success argument.

open as a page

In PHP, why should you time a block of code with hrtime(true) instead of microtime(true), and how do you turn the result into milliseconds?

level: juniorimportance: should knowfreq 38%

basics

~20 s

hrtime(true) reads a monotonic clock in nanoseconds, so the difference of two calls is a true elapsed time; microtime(true) reads the adjustable wall clock as float seconds. Subtract two hrtime(true) values and divide by 1e6 for milliseconds.

open as a page

In PHP, what does the realpath cache store, and how do realpath_cache_size, realpath_cache_ttl and open_basedir affect it?

level: middleimportance: should knowfreq 26%

basics

~20 s

The realpath cache stores resolved absolute paths for files and directories PHP touches, saving repeated filesystem lookups. It is sized by realpath_cache_size (default 4M), expires entries after realpath_cache_ttl (default 120 s), and is disabled when open_basedir is set.

open as a page

In PHP, when does passing or assigning a large array actually duplicate its memory, and which values are refcounted at all?

level: middleimportance: should knowfreq 32%

basics

~20 s

Assigning or passing an array only shares it; PHP duplicates the table when one holder writes while others still share it. Integers, floats, booleans and null live inside the zval uncounted; strings, arrays and objects are refcounted.

open as a page

In PHP, what do memory_get_usage() and memory_get_peak_usage() report, and what does passing true for $real_usage change?

level: middleimportance: should knowfreq 38%

basics

~20 s

memory_get_usage() returns the bytes PHP's memory manager has allocated to the script now; memory_get_peak_usage() the highest so far. With $real_usage = true both report memory reserved from the system in chunks, the figure memory_limit is checked against.

open as a page

In PHP, how do you size opcache.memory_consumption and opcache.max_accelerated_files, and how do you tell that OPcache is full?

level: middleimportance: should knowfreq 30%

basics

~20 s

Set opcache.max_accelerated_files above the number of PHP files the app can load, vendor included, and opcache.memory_consumption (default 128 MB) with headroom over measured use. opcache_get_status() shows a full cache as cache_full, low free memory, or rising oom_restarts or hash_restarts.

open as a page

In a PHP profiler's call graph, what is the difference between inclusive and exclusive time, and which one tells you what to optimize?

level: middleimportance: should knowfreq 35%

basics

~20 s

Inclusive time includes everything a function called; exclusive (self) time is its own body only. Follow inclusive time down to find the costly branch, then exclusive time and call counts to find the code to change.

open as a page

When profiling PHP code, how does an instrumenting profiler differ from a sampling profiler, and why can instrumentation mislead you about where time goes?

level: middleimportance: should knowfreq 32%

basics

~20 s

An instrumenting profiler hooks every PHP function entry and exit, giving exact call counts but adding cost per call; a sampling profiler records the current call stack at a fixed interval, with low, predictable overhead but only statistical results.

open as a page

In PHP, a request takes 900 ms of wall time but its getrusage() CPU time is only 60 ms; what does that gap mean?

level: middleimportance: should knowfreq 30%

basics

~20 s

Wall time is elapsed real time; CPU time is what the PHP process spent executing, from getrusage() as user plus system time. A 900 ms versus 60 ms gap means the request mostly waited on I/O.

open as a page

In PHP, what does apcu_entry() guarantee when many requests miss the same cached feature-flag list at once, and what does its lock cost?

level: seniorimportance: should knowfreq 24%

basics

~20 s

apcu_entry($key, $callback, $ttl) returns the cached value or runs the callback once and caches the result, so concurrent misses do not all rebuild it. The callback runs under APCu's exclusive cache lock, blocking every other APCu call on the server.

open as a page

In APCu for PHP, what happens when apc.shm_size fills up, and how do apc.ttl and per-entry TTLs change what gets evicted?

level: seniorimportance: should knowfreq 22%

basics

~20 s

When APCu's shared memory (apc.shm_size, default 32M) is full, it first removes expired entries if it can; if apc.ttl is 0 (the default) or that frees too little, it clears the entire cache, causing a burst of misses.

open as a page

In a long-running PHP script, when does the cycle collector run on its own, and when is calling gc_collect_cycles() worth it?

level: seniorimportance: should knowfreq 25%

basics

~20 s

Refcounting frees most values at once; the cycle collector handles arrays and objects that reference each other. It runs when its possible-root buffer reaches a threshold (10,000 by default, adapted after each run); call gc_collect_cycles() at batch boundaries to free cycles sooner.

open as a page

A PHP import script dies with "Allowed memory size exhausted" after about 200,000 rows; what usually holds the memory, and how do you fix it without raising memory_limit?

level: seniorimportance: should knowfreq 45%

basics

~20 s

The script usually holds every row at once: file() or fetchAll(), a buffered result set, or an array collecting processed records. Stream instead: read lines one by one, fetch rows singly or in keyed batches, and keep nothing per row.

open as a page

After a PHP deploy on servers with opcache.validate_timestamps=0, the site keeps serving the old code; why, and how should the deploy clear OPcache?

level: seniorimportance: should knowfreq 40%

basics

~20 s

With validation off, OPcache never rereads changed files, so old compiled scripts keep running. Clear the cache inside its owner: call opcache_reset() through the web server's PHP, or restart or reload PHP-FPM; a CLI opcache_reset() cannot reach it.

open as a page

In PHP 8.5, how do you turn on the JIT compiler, and which workloads actually get faster with it?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Set opcache.jit=tracing (the recommended mode) with OPcache enabled; since PHP 8.4 the default is opcache.jit=disable with a 64M opcache.jit_buffer_size. The JIT speeds up CPU-bound PHP such as numeric loops and parsers; typical web requests waiting on databases gain little.

open as a page

In PHP, what does WeakReference::create() return, and what does its get() method give back once the object is destroyed?

level: middleimportance: nice to knowfreq 12%

basics

~20 s

WeakReference::create($obj) returns a WeakReference that points at the object without increasing its refcount. While the object lives, get() returns it; once the last normal reference is gone and the object is destroyed, get() returns null.

open as a page

In PHP, what does opcache.preload do, and what do you give up when you preload an application's classes?

level: seniorimportance: nice to knowfreq 18%

basics

~20 s

opcache.preload runs a script once at server start; the classes, functions, interfaces and traits it loads stay in shared memory and exist in every request without autoloading. The price: changes need a server restart, and the memory is always held.

open as a page