Skip to main content
Guide

What happens when PHP-FPM runs out of memory in production

The Reflex Team9 min15 May 2026

The site was fine at lunch. By 2pm, product pages load intermittently. By 2:20pm, nginx returns 502 Bad Gateway on every request. CPU is oddly low. RAM looks tight. In /var/log/php8.3-fpm.log you find the tell: server reached pm.max_children setting followed by Linux OOM killer entries in dmesg. PHP-FPM did not run out of memory in the abstract — it ran out of workers, then the box ran out of RAM, then everything died.

This is one of the most common Laravel production incidents on single-server deployments. Understanding the cascade helps you fix it fast and prevent recurrence.

Stage 1: Slow requests pile up

A new deploy introduced an N+1 query on a catalog page. Each request now takes eight seconds instead of two hundred milliseconds. FPM workers stay busy. The listen queue grows — nginx connections wait for free workers. Users see spinners, then timeouts.

If request_slowlog_timeout is enabled, the slow log names the offender:

request_slowlog_timeout = 5s
slowlog = /var/log/php-fpm/www-slow.log

Tail it:

sudo tail -f /var/log/php-fpm/www-slow.log

Fix the code path — eager load relationships, cache heavy aggregates — but production may need immediate relief.

Stage 2: pm.max_children reached

PHP-FPM is configured with a maximum concurrent worker count. When all workers are busy, new requests queue at the socket. Log lines repeat:

WARNING: [pool www] server reached pm.max_children setting (20), consider raising it

Raising max_children without math is dangerous. Each worker can consume 128–512MB for heavy Laravel apps. Twenty workers at 256MB is 5GB before nginx, MySQL, and Redis. Use the PHP-FPM calculator to size pools from available RAM and measured per-request memory.

Typical pool config in /etc/php/8.3/fpm/pool.d/www.conf:

pm = dynamic
pm.max_children = 20
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 6
pm.max_requests = 500

pm.max_requests recycles workers leaking memory — omit it and a slow leak eventually kills the box.

Stage 3: nginx 502/504 to users

From outside, the site is down. nginx error log shows:

upstream timed out (110: Connection timed out) while reading response header from upstream
connect() to unix:/run/php/php8.3-fpm.sock failed (11: Resource temporarily unavailable)

Customers cannot tell FPM from database failure — it is just broken checkout. Generic uptime monitors hitting / might still succeed briefly if one worker eventually frees — making the incident feel intermittent and harder to debug.

Stage 4: kernel OOM kill

If workers grow too numerous or leak RAM, the Linux OOM killer targets heavy PHP processes. dmesg shows:

Out of memory: Killed process 12345 (php-fpm8.3) total-vm:524288kB

Now FPM enters crash loops. Recovery requires restart and often rollback of the triggering deploy.

Use the OOM analyser to estimate headroom from your server RAM, worker count, and observed per-request memory — before you swap 502s for hard OOM.

Diagnosis checklist

Run in order during an incident:

  1. sudo systemctl status php8.3-fpm — active or failed?
  2. grep max_children /var/log/php8.3-fpm.log | tail -20
  3. df -h && free -m — disk and memory headroom
  4. FPM slow log — which script hangs?
  5. dmesg | tail -50 — OOM kills?
  6. Deploy timeline — did release precede errors by minutes?

Correlate with Horizon and MySQL slow query log if the endpoint touches queues or heavy reads.

Manual fixes under fire

Immediate relief — restart FPM to free stuck workers:

sudo systemctl restart php8.3-fpm

Rollback if a deploy caused the regression:

# your deploy tool's rollback — Forge, Envoyer, or git revert + redeploy

Reduce concurrency temporarily — lower pm.max_children only if RAM is critically low and you accept queueing; better fix the slow endpoint.

Raise limits safely — after calculator math, bump max_children, reload:

sudo systemctl reload php8.3-fpm

Set PHP memory limit intentionally — memory_limit = 256M in php.ini prevents one runaway request from eating the entire box; pair with app-level fixes.

Enable opcache and validate opcache.validate_timestamps strategy after deploys — stale or cold opcache patterns affect worker behavior post-release.

Prevention for Laravel teams

  • Load test catalog and checkout after major releases
  • Monitor FPM listen queue and active processes, not just HTTP
  • Alert on sustained max_children log lines before OOM
  • Recycle workers with pm.max_requests
  • Keep logrotate healthy so disk does not compound memory pressure

Profile memory in staging with realistic data volumes — seed databases lie. Use php artisan route:list and integration tests on heavy endpoints before marketing sends traffic.

When incidents repeat weekly, schedule a code fix sprint — automation is a bandage on leaking max_children from uncapped imports.

Worked example: CSV import on a 2GB Droplet

A client uploads 50MB CSV for processing in one request. PHP memory_limit allows 256MB but the import peaks at 400MB — worker swells, siblings cannot spawn, pool saturates. Fix: chunk imports to queue jobs, lower web memory_limit, raise worker memory on a dedicated queue worker service. Calculator + OOM analyser prevent guessing new max_children.

How Reflex helps

reflexd watches PHP-FPM pool saturation, worker crashes, and memory pressure. Brain playbooks can restart FPM or bounce workers when policy allows — before customers flood support. Deploy markers tie regressions to releases for faster rollback decisions.

Start a trial. Laravel monitoring guide. How the Brain repair cycle works. Agencies: Reflex for agencies.

Ready to stop firefighting your servers?

Try Reflex free for 14 days.