
PHP-FPM (FastCGI Process Manager) is an alternative PHP FastCGI implementation that has become the de facto standard for running PHP on production servers. Unlike the older mod_php Apache module that embeds a PHP interpreter inside every Apache worker process, PHP-FPM runs PHP as a completely separate pool of worker processes. Your web server — whether Nginx or Apache with mod_proxy_fcgi — hands off PHP requests to PHP-FPM over a Unix socket or TCP connection, and PHP-FPM returns the rendered response.
This separation has profound performance implications. Your web server can continue serving static files, handling TLS, and managing connections without being blocked by PHP execution. PHP worker processes can be recycled after a configurable number of requests, preventing memory leaks from accumulating indefinitely. And most importantly for high-traffic scenarios, you can precisely control how many PHP processes are alive at any given moment, which directly governs your server's throughput and memory consumption.
On an under-tuned server, PHP-FPM defaults are often dangerously conservative. A freshly installed PHP 8.2 pool on Ubuntu 22.04 ships with pm = dynamic, pm.max_children = 5, and pm.max_spare_servers = 3. On any real-world website receiving more than a few dozen concurrent visitors, those five children will be permanently saturated, new requests will queue, and response times will balloon. Getting these numbers right is the single highest-leverage PHP performance optimization available to you.
PHP-FPM offers three process manager (pm) modes, and choosing the right one is the first tuning decision you will make. Each mode has a distinct operational profile suited to different traffic patterns.
static — PHP-FPM spawns exactly pm.max_children worker processes at startup and keeps them all running permanently, regardless of load. There is no warm-up latency for spikes because every worker is always ready. The trade-off is that all reserved memory is consumed even at 3 AM when traffic is near zero. Static mode is the right choice for servers dedicated to high-traffic applications where RAM is abundant and you want absolutely predictable behaviour. It eliminates the overhead of process management logic entirely.
dynamic — PHP-FPM maintains a minimum number of idle workers (pm.min_spare_servers) and scales up to pm.max_children on demand, then scales back down to the spare server range during quiet periods. This is the most commonly used mode because it balances memory efficiency with responsiveness. The tuning knobs — pm.start_servers, pm.min_spare_servers, and pm.max_spare_servers — require careful calibration to avoid both under-provisioning and memory waste.
ondemand — Workers are spawned only when a request arrives and are killed after pm.process_idle_timeout seconds of inactivity. This mode is ideal for low-traffic or development servers where conserving RAM is paramount. For high-traffic production sites, ondemand is generally a poor choice because each cold worker spawn adds latency to the first request it serves.
For most high-traffic sites, static mode is the safest recommendation once you have calculated the correct pm.max_children value. It eliminates process management overhead and guarantees a consistent number of available workers at all times. Reserve dynamic mode for servers hosting multiple pools with different traffic profiles.
The most critical PHP-FPM tuning parameter is pm.max_children. Set it too low and requests queue up under load, causing timeouts. Set it too high and your server runs out of RAM, triggering the OOM killer — which is worse than any timeout. The calculation is straightforward once you know two numbers: the total RAM available to PHP-FPM, and the average memory footprint of a single PHP worker process.
First, determine how much RAM each PHP worker actually uses. The most accurate method is to observe your running processes under real load:
A typical WordPress or Laravel worker on PHP 8.2 consumes between 30 MB and 80 MB of RSS memory depending on the number of loaded extensions, OPcache settings, and application complexity. A lean application with OPcache enabled will sit closer to 30–40 MB. A feature-heavy WooCommerce site with many active plugins can push 70–100 MB per worker.
Always reserve RAM for the operating system kernel, your web server, database, and other services. On a 4 GB VPS running Nginx and MySQL, a practical rule of thumb is to allocate no more than 2.5–3 GB to PHP-FPM. On a dedicated 16 GB server, you might allocate 10–12 GB after accounting for OS, Nginx, and a small MySQL buffer pool. After measuring worker RSS and reserving overhead, divide and floor the result — that is your pm.max_children.
When you choose dynamic mode, four parameters work together to govern pool behaviour. Here is a complete, production-hardened pool configuration for a busy website on a 4 GB server where each PHP worker averages 50 MB:
Walk through each dynamic parameter in context. pm.start_servers determines how many workers PHP-FPM spawns when the service starts. Setting this equal to the midpoint of your spare server range avoids both a cold start with zero workers and over-provisioning at boot. The value should be between pm.min_spare_servers and pm.max_spare_servers.
pm.max_requests = 500 instructs each worker process to handle 500 requests and then gracefully exit, after which a new worker is spawned to replace it. This is your primary defence against PHP memory leaks. Without it, long-running workers accumulate leaked memory from poorly written extensions or application code until they consume gigabytes. Setting this between 200 and 1000 is reasonable — lower values reduce leak exposure at the cost of slightly more process churn. CloudStick's EasyPHP environment sets pm.max_requests to 500 by default for exactly this reason.
request_terminate_timeout sets the maximum wall-clock time a single PHP request may run before PHP-FPM sends SIGKILL to the worker. This prevents runaway scripts from occupying a worker slot indefinitely. For most web applications, 30–60 seconds is appropriate. If you run background-style scripts through the web, consider separating them into a dedicated pool with a higher timeout.
Two built-in PHP-FPM facilities are indispensable for ongoing performance monitoring: the slow log and the status page. Both are disabled by default and require explicit configuration.
The slow log works similarly to MySQL's slow query log. When request_slowlog_timeout is set to a nonzero value, PHP-FPM captures a full stack trace via ptrace(2) for any request that exceeds the threshold. This is exceptionally powerful for identifying which specific functions or database queries are responsible for slow responses, without any application-level code changes. The output looks like this:
The status page provides a real-time snapshot of pool health. Once you have configured pm.status_path = /fpm-status in the pool and exposed it via your Nginx config, you can poll it with a simple curl call:
The single most important counter on the status page is max children reached. Every time this counter increments, it means PHP-FPM hit the ceiling of pm.max_children and a request had to wait in the listen queue. If this number is nonzero and growing, your pool is under-provisioned and you either need to increase pm.max_children (by adding RAM or by reducing per-worker memory consumption) or horizontally scale to additional servers.
PHP-FPM supports graceful reloads via SIGUSR2, which tells the master process to respawn workers with the new configuration without dropping any in-flight requests. This is the correct way to apply configuration changes on a live server — never use a hard restart unless you are comfortable with a brief period of 502 errors.
After reload, use watch to observe the status page in real time while your load test runs. You are looking for the listen queue to remain at zero, active processes to scale up smoothly without hitting pm.max_children, and idle processes to stay within the spare server range during quiet periods. If the listen queue stays at zero and max children reached does not increment during your peak simulation, your configuration is solid.
Check your system journal for any OOM events that might indicate you have over-provisioned workers relative to available RAM:
PHP-FPM tuning is an iterative process. Start with conservative values derived from your memory calculations, observe under real or simulated load, and adjust upward or downward based on what the status page and slow log tell you. The combination of a correctly sized pool, OPcache properly configured, and pm.max_requests set to prevent memory leaks will carry most PHP applications through traffic volumes that would otherwise require expensive horizontal scaling. On a well-tuned 4 GB server, it is entirely realistic to handle several hundred simultaneous PHP requests with sub-100ms response times — the defaults will never get you there, but the mathematics will.
Never increase pm.max_children without first verifying you have sufficient free RAM. If your workers consume more memory than is physically available, the Linux OOM killer will terminate processes at random — including PHP-FPM master, MySQL, or even your web server. Always leave at least 200–400 MB of headroom for kernel buffers and page cache.

