PHP-FPM: Dynamic vs OnDemand vs Static

Published · 3 views
PHP-FPM: Dynamic vs OnDemand vs Static

PHP-FPM ships with three process manager modes — static, dynamic, and ondemand — and the default pm = dynamic setting in most distro configs is rarely the right call once a site has to survive real traffic on a budget VPS. I've rebuilt this exact config more times than I can count on client Laravel and WordPress boxes, usually right after a "why did the server run out of memory at 2am" ticket, and the fix is almost always the same: the pm mode was never actually chosen, it was just inherited from whatever the install script wrote.

This post is the comparison I wish existed the first time I had to make this call: what each mode actually does under load, where it breaks, and which one I'd pick for a given box size — not a restatement of the PHP manual.

What pm actually controls

PHP-FPM's pm directive decides how child worker processes get created and destroyed. Every PHP request your web server hands off to FPM gets picked up by one of these workers, and each worker holds its own chunk of memory — this is the number that actually determines whether your server survives a traffic spike or falls over with an OOM kill.

The three modes answer the same question differently: how many workers should exist right now, and how aggressively should FPM spin new ones up or tear idle ones down?

static — a fixed worker count, no surprises

; /etc/php/8.3/fpm/pool.d/www.conf
pm = static
pm.max_children = 12

With static, FPM starts exactly pm.max_children workers at boot and keeps them running forever, busy or idle. There's no spin-up delay for the 13th request because there is no 13th worker to spin up — it queues until one frees up.

This is the mode I reach for on a dedicated box with memory to spare and a workload that's consistently busy (a high-traffic API, not a marketing site that gets three requests an hour). The tradeoff is upfront: you're paying for max_children × average_worker_memory in RAM at all times, whether anyone's visiting or not.

dynamic — a sliding range with five extra knobs

pm = dynamic
pm.max_children = 10
pm.start_servers = 3
pm.min_spare_servers = 2
pm.max_spare_servers = 6

dynamic tries to be adaptive: it starts with start_servers, grows toward max_children under load, and shrinks back down when traffic eases — but never below min_spare_servers. On paper this sounds like the best of both worlds. In practice it's the mode most people copy-paste without tuning, and it's also the one most likely to be misconfigured, because now you've got five interdependent numbers instead of one.

Here's where most tutorials stop short: they tell you to set pm.max_children based on total RAM / average process size and move on. That math is right, but it ignores that min_spare_servers workers sit idle 24/7 consuming memory even on a site with almost no traffic — which is exactly the scenario ondemand was built for.

ondemand — spin workers up from zero, kill them when idle

pm = ondemand
pm.max_children = 10
pm.process_idle_timeout = 10s

ondemand keeps zero workers running when there's no traffic and spawns them the moment a request arrives, killing each one off after process_idle_timeout seconds of inactivity. The cost is a few milliseconds of process-spawn latency on a cold request — usually irrelevant unless you're running something latency-sensitive behind a load balancer with aggressive health checks.

I'd skip dynamic entirely on anything smaller than a 4GB VPS and go straight to ondemand. On a handful of client sites running on 1-2GB droplets, switching from dynamic to ondemand dropped idle memory usage by 150-300MB — the exact margin that was causing MySQL and PHP-FPM to fight over RAM and trigger the OOM killer during traffic bursts.

Picking a mode: a direct comparison

static dynamic ondemand
Idle memory use High (fixed) Medium (min_spare floor) Near zero
Cold-start latency None None for spares, some beyond Small, on every scale-up
Config complexity 1 directive 5 directives 2 directives
Best for Busy, dedicated boxes Mid-traffic, well-tuned boxes Small VPS, bursty/low traffic
Worst case Wastes RAM when idle Misconfigured spares waste RAM too Slightly slower first request after idle

Working out pm.max_children for real

Whichever mode you land on, pm.max_children is the number that actually matters, and it's a function of your server's RAM and your app's real memory footprint — not a round number picked because it looked reasonable.

# Check average FPM worker memory in use right now
ps -ylC php-fpm8.3 --sort:rss | awk '{print $8}' | tail -n +2

Take that average (call it A in MB), subtract what the rest of the stack needs (MySQL, Redis, Nginx, the OS itself — budget at least 400-600MB for that on a small box), and divide the remainder by A. On a 2GB droplet with a 70MB average Laravel worker and 600MB reserved for everything else, that's roughly (2048 - 600) / 70 ≈ 20 as an upper ceiling — I'd still set pm.max_children a bit under that, not right at the edge, because traffic spikes and memory spikes rarely stay this tidy in practice.

Frequently Asked Questions

My site crashes under load even with ondemand set — why? Usually pm.max_children is set too high for the box's actual RAM, so FPM happily spawns workers right up until the kernel's OOM killer steps in. Rerun the memory math above using real numbers from ps, not a guess.

Does switching pm mode require a full server reboot? No — just sudo systemctl reload php8.3-fpm (or restart if reload doesn't pick up pool changes on your distro's FPM build). No downtime beyond the brief worker respawn.

Why does my app feel slightly slower right after a quiet period on ondemand? That's the cold-start cost — FPM has to fork a fresh worker for the first request after process_idle_timeout kills the idle ones. If that latency genuinely matters for your use case, raise process_idle_timeout so workers survive longer between bursts, or move to dynamic with a small min_spare_servers floor.

Can I run multiple pools with different pm modes on the same server? Yes, and it's underused — a pool serving a webhook endpoint that needs to always be warm can run static, while a pool serving the admin dashboard on the same box runs ondemand. Each pool gets its own .conf file in pool.d/.

Where I'd land on this

For anything under 4GB of RAM: use ondemand, tune pm.max_children from real worker memory numbers, and move on — it's the mode with the fewest ways to accidentally waste memory you don't have. For a dedicated, consistently busy box, static at a number you've actually calculated beats dynamic's complexity for no real latency win. I'd only reach for dynamic on a mid-size box where traffic genuinely varies through the day and you're willing to actually tune all five of its directives — not just copy the defaults and hope.

#nginx #server-configuration #linux #php-fpm #performance-tuning
Aliyan Faisal

Written by

Aliyan Faisal

Full-stack developer and AI/LLM systems engineer. I build LLM integrations, RAG pipelines and automations, and the web apps and servers behind them.

0 Comments

No comments yet — be the first to share your thoughts.

Leave a comment

Never published.