skip to content

Refcounting & Cycle Collection

PHP frees most values when their refcount hits zero, copies arrays and strings only on write, and runs a cycle collector for the rest. Interviewers probe leaks and memory_limit errors.

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

explore

questions

6

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, 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 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

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