How to diagnose and fix PHP-FPM memory leaks on Linux servers
By Kevin Marcondes · Published on · Updated on · 8 min read
If your PHP-FPM workers keep growing until the OOM Killer steps in, you may be facing a PHP-FPM memory leak or OpCache/heap fragmentation. Learn how to confirm the cause in production, reproduce safely in staging, and apply tuning and mitigations that stabilize the environment while a permanent fix is implemented.
The problem: OOM Killer, crashes and 502/503 errors
Memory leaks in PHP-FPM typically show as continuous RSS growth per worker, high swap use, slowness and 502/503 errors under load. First, identify the incident pattern and determine whether the issue is due to excessive concurrency (sizing) or per-process retention/fragmentation.
Use the signals below as initial clues to decide the next diagnostic steps.
| Symptom | Likely cause | Next step |
|---|---|---|
| OOM Killer kills php-fpm | pm.max_children too high or leak in workers | Measure RSS per worker; adjust pm and memory_limit; enable rotation (pm.max_requests) |
| Workers' RSS grows continuously | Heap/OpCache fragmentation or unreleased objects/C extension | Inspect /proc/PID/smaps_rollup and pmap -x; watch Private_Dirty |
| High swap and latency | Insufficient effective RAM or pool oversizing | Review max_children; consider reducing concurrency/optimize heavy endpoints |
| 502/503 errors during spikes | FPM queue depleted/timeout due to saturation | Enable status_path; measure the queue; correlate with the heaviest routes |
Probable causes of PHP-FPM memory leaks
In pure PHP, the garbage collector frees most objects at the end of each request. Abnormal per-worker memory growth typically comes from:
Not every rise in memory is a leak: requests that process large files, images or big arrays can generate memory spikes without retention. Differentiate a spike (high VmHWM) from retention (VmRSS rises and doesn’t return).
- C extensions (image, XML, crypto) that don’t release buffers on certain code paths.
- Heap and OpCache fragmentation: many classes/files create fragmentation and retention, even without a logical “leak.”
- Static caches/singletons that grow with catalog size or accessed routes.
- Reference cycles and objects tied to external resources that the GC doesn’t collect well.
- Workers that live too long without recycling (pm.max_requests set very high or 0).
Quick production diagnosis with ps, top and /proc
Goal: confirm whether the issue affects a few workers or the whole pool, and whether memory is retained between requests. Focus on low-cost metrics that don’t cause downtime. Avoid invasive changes during business hours.
Correlate spikes with routes/tenants using structured logs. If you don’t have them yet, see the guide on structured logs and correlation.
# Visão rápida de consumo de memória
free -h && swapon --summary
# Quais processos consomem mais RSS (memória residente)
ps -o pid,ppid,cmd,%mem,rss --sort=-rss -C php-fpm | head
# Inspecione um worker específico ($PID)
PID=<um_pid_de_worker>
cat /proc/$PID/status | egrep 'VmRSS|VmHWM|Threads'
# Resumo de mapas de memória do processo
cat /proc/$PID/smaps_rollup | egrep 'Rss|Pss|Private'
Mapping allocations: pmap and smaps analysis
If a worker grows after each request, check where memory is allocated (anonymous vs mapped, private vs shared). Look for large Private_Dirty regions (heap) and continuous anonymous RSS growth.
Combine pmap (summary) with smaps_rollup (totals by type). Do this on a single worker under low load.
PID=<um_pid_de_worker>
# Top 20 regiões por RSS no processo
pmap -x $PID | sort -k3 -n | tail -n 20
# Resumo consolidado (verifique Private_* e Anonymous)
cat /proc/$PID/smaps_rollup | egrep 'Anonymous|Shared|Private|Rss|Pss'
Watching memory syscalls with strace (mmap/brk)
When RSS increases but you want to confirm active allocations, observe memory-related syscalls. strace adds overhead — run it on a single worker for a short time, preferably outside peak hours.
PID=<um_pid_de_worker>
# Observar syscalls de memória (mmap/brk/munmap/mremap)
strace -p $PID -e trace=brk,mmap,munmap,mremap -f -tt -o /tmp/trace.$PID.log &
# Gere algumas requisições representativas... depois
sleep 10; kill %+
# Consulte o log em busca de alocações que não recebem munmap correspondente
awk '/mmap\(/ {m++} /munmap\(/ {u++} END {print "mmap:",m, "munmap:",u}' /tmp/trace.$PID.log
Reproducing in staging with Valgrind and Xdebug
Ideally reproduce the issue outside production with a script that hits the problematic endpoint many times. Valgrind Massif identifies heap peaks and growth. Xdebug traces show per-function memory spikes. Do not run Valgrind on production workers.
# 1) Script reprodutor simples (CLI)
cat > repro.php <<'PHP'
<?php
function hit() {
// Simule chamada ao código/endpoint alvo
$data = file_get_contents('http://localhost/rota-pesada');
return strlen($data);
}
for ($i = 0; $i < 500; $i++) {
hit();
if ($i % 10 === 0) {
fprintf(STDERR, "it=%d rss=%d peak=%d\n", $i, memory_get_usage(), memory_get_peak_usage());
}
}
PHP
# 2) Heap profiling com Valgrind Massif
valgrind --tool=massif --time-unit=B --massif-out-file=massif.out \
php -d detect_leaks=1 repro.php
ms_print massif.out | head -n 100
# 3) Trace de memória por função com Xdebug (em CLI)
php -d xdebug.mode=trace -d xdebug.start_with_request=yes \
-d xdebug.output_dir=. repro.php
ls -1 cachegrind* trace*
Immediate PHP-FPM and PHP tuning and mitigations
Mitigate the OOM risk while investigating: recycle workers, limit spikes and tune pool size. Document changes and measure impact.
Practical sizing: max_children ≈ (RAM available to FPM) / (average RSS per worker under load). Measure RSS after warmup and representative traffic.
Also review OpCache: fragmentation can amplify memory use. See common parameters below and the PHP-FPM documentation, memory_limit and OpCache docs.
- Enable recycling: set pm.max_requests between a few hundred and a few thousand, depending on load and acceptable latency.
- Tighten memory_limit at the pool level (php_admin_value) to contain outliers; tune per heavy route/job as needed.
- If FPM backlog grows, reduce max_children and increase per-request capacity (optimize code) instead of only boosting concurrency.
- Monitor queue and latency after each change; if performance regresses, revisit the configuration.
; /etc/php-fpm.d/www.conf (exemplos)
pm = dynamic
pm.max_children = 20
pm.start_servers = 4
pm.min_spare_servers = 4
pm.max_spare_servers = 10
pm.max_requests = 1000
pm.process_idle_timeout = 10s
request_terminate_timeout = 60s
php_admin_value[memory_limit] = 256M
; php.ini / opcache
opcache.enable=1
opcache.validate_timestamps=1
opcache.memory_consumption=256
opcache.max_accelerated_files=100000
opcache.interned_strings_buffer=16
opcache.jit_buffer_size=0 ; se JIT causar pressão de memória
Best practices and continuous monitoring
- Log memory_get_peak_usage by route and correlate with request-id. See the structured logs and correlation guide.
- Track RSS/Private_Dirty per worker and create alerts before OOM occurs.
- Adopt an APM to detect endpoints that raise allocations; compare solutions in the observability for PHP guide.
- Add memory regression tests to CI (CLI script that runs the case for N iterations). Base this on CI/CD with PHP.
- In legacy apps, isolate hot paths with tests; see ideas in the PHP legacy testing guide.
7-step suggested action plan
1) Confirm impact: RSS per worker, swap and FPM queue. 2) Identify whether growth is global or route-specific. 3) Collect smaps_rollup/pmap from a target worker. 4) Enable pm.max_requests to stabilize. 5) Reproduce in staging and run Valgrind/Xdebug. 6) Apply fixes in code/extension/config. 7) Monitor after changes for several traffic cycles.
Frequently asked questions
How to tell a real leak from a legitimate memory spike?
A real leak shows VmRSS growing and not returning to baseline after each request, cycle after cycle. Legitimate spikes raise VmHWM (historic max) but VmRSS falls back at the end. Compare smaps_rollup, collect inter-request metrics and watch whether Private_Dirty grows monotonically.
Does pm.max_requests fix a leak?
No — it doesn’t fix the root cause, but recycling workers limits the lifetime of a leaking or fragmented process, reducing the OOM risk. Choose a value that balances stability and cold-start latency and monitor for response-time fluctuations after the change.
Can OpCache increase memory usage?
Yes. Fragmentation and growth of the script cache and interned strings raise RSS, especially in apps with many files/classes. Tune opcache.memory_consumption and max_accelerated_files, and recycle workers periodically. Also check whether frequent deploys without FPM restart leave stale cache fragments.
Is it safe to use strace and Valgrind in production?
strace adds overhead; use it on a single worker for a short time and outside peak hours. Valgrind is not recommended in production due to its high overhead and potential to destabilize the service. Reproduce in staging with synthetic traffic to obtain heap profiles and function traces.
How to estimate pm.max_children with limited RAM?
Measure the average RSS per worker under representative load (after warmup). Subtract RAM needed by the OS and other services from total RAM. Divide the remaining RAM by the average RSS and apply a safety margin. If the result is very low, prioritize per-request optimizations over increasing concurrency.
Want to stabilize PHP-FPM and avoid OOMs?
Discuss a safe diagnostic and mitigation plan for your environment: identify root cause, tune FPM and monitor without disrupting service.
Discuss PHP-FPM diagnosisSee also
- How to recover corrupted statistics and indexes in PostgreSQL that caused slow queries
- REST API latency spikes from N+1 queries and PHP GC — how to diagnose and fix
- Postgres 'Too many connections' at billing peak — practical diagnosis and remediation guide
- Reduce costs migrating Magento to OpenMage with a rollback plan and acceptance tests