Innovate | Connect | Transform
ContactService statusUSD $

GICT knowledge base

PHP-FPM Configuration Guide: How to Tune Pools for Maximum Server Performance

Published

System Administration

Most PHP servers run on default PHP-FPM settings, and most of them pay for it. The defaults are deliberately conservative, sized for a tiny shared box rather than the server you actually run. The result is a machine with plenty of memory sitting idle while requests queue up behind a handful of worker processes, or, just as often, a server that quietly tips over under load because it spawned more workers than its memory could hold.

PHP-FPM, which stands for FastCGI Process Manager, is the piece that runs your PHP code behind a web server like Nginx. Tuning it well is one of the highest-return things you can do for a PHP application, and it usually takes nothing more than understanding a handful of settings in one configuration file. This guide explains how the process pool works, how to calculate the right number of workers for your hardware, and how to set the process manager so it matches your traffic instead of fighting it.

Prerequisites

Before you start, make sure you have:

  • A Linux server running PHP-FPM, version 8.3 or later, behind Nginx or Apache.
  • Root or sudo access to edit the pool configuration file, usually found at /etc/php/8.4/fpm/pool.d/www.conf on Debian and Ubuntu systems.
  • The ability to restart the PHP-FPM service after changes.
  • A rough idea of your server’s total memory, which you can check with the free -m command.

One note before the settings. PHP 8.5 is the current release and PHP 8.4 is the recommended production target, supported into 2028. The tuning advice here works the same across these versions, but running a supported version matters, because an end-of-life PHP receives no security patches and misses the steady performance gains each release brings. If you are still on 8.1 or 8.2, plan that upgrade alongside this work.

How the PHP-FPM Process Pool Works

You cannot tune what you do not understand, so start with the model. PHP-FPM runs as a master process that does no request handling itself. Its only job is to manage a pool of child processes, called workers. The web server hands an incoming PHP request to the master, and the master passes it to a free worker.

Here is the detail that drives every decision that follows: each worker handles exactly one request at a time, and each worker holds its own chunk of memory for as long as it lives. Ten workers means ten requests served at once, and it also means ten copies of PHP’s memory footprint sitting in random-access memory (RAM). That trade-off, between concurrency and memory, is the entire tuning problem in one sentence. More workers serve more simultaneous traffic, right up until they exhaust your memory, at which point performance collapses.

So the goal is not “as many workers as possible.” The goal is the largest number of workers your memory can comfortably hold, and a process manager mode that grows and shrinks the pool to match real traffic. Everything below is in service of those two things.

1. Choose the Right Process Manager Mode

The single most important setting in the pool is pm, which selects how the process manager decides how many workers to keep running. It has three modes, and picking the wrong one wastes either memory or response time.

The static mode keeps a fixed number of workers alive at all times, defined by pm.max_children. It uses the most memory because every worker exists whether traffic needs it or not, but it is also the fastest under steady load, since a worker is always ready and no time goes into spawning new ones. This is the right choice for a dedicated, consistently busy server where you want predictable performance and you have sized the pool to fit memory.

The dynamic mode keeps a baseline of idle workers and spawns more up to a ceiling as traffic rises, then retires the extras when things quiet down. It balances memory against responsiveness and works well for most general-purpose servers with variable traffic. It is the sensible default when you are not sure.

The ondemand mode starts with zero workers and spawns them only when requests arrive, killing them off after an idle timeout. It uses the least memory, which makes it ideal for low-traffic sites or servers hosting many small applications at once. The cost is a small delay on the first request after an idle period, while a worker spins up.

My honest take, since guides love to hedge here: pick dynamic unless you have a specific reason not to. Move to static only once you have measured a steady, high load and want to squeeze out the last bit of latency. Use ondemand for the long tail of quiet sites where memory matters more than that first-request delay.

pm = dynamic

2. Calculate pm.max_children From Real Memory Usage

This is the setting that makes or breaks a server, and it is the one people guess at most often. pm.max_children is the hard ceiling on how many workers can run at once. Set it too low and requests queue even though the server has memory to spare. Set it too high and a traffic spike spawns workers until memory runs out, the system starts swapping to disk, and the whole server grinds to a halt. The second failure is far worse than the first.

Do not guess this number. Calculate it. The formula is simple:

pm.max_children = (Memory available for PHP) / (Average memory per worker)

Find the average memory per worker by looking at running processes under load. This command shows the memory footprint of your PHP-FPM workers in megabytes:

ps --no-headers -o rss -C php-fpm8.4 | awk '{ sum += $1 } END { print sum/NR/1024 " MB" }'

Suppose that returns 40 megabytes per worker. On a server with 8 gigabytes of memory, you cannot hand all 8 to PHP, because the operating system, the web server, and any database on the same box need their share. Reserve a generous slice for them, say 2 gigabytes, which leaves 6 gigabytes, or roughly 6000 megabytes, for PHP. Divide 6000 by 40 and you get 150 workers as your ceiling.

pm.max_children = 150

Two cautions that save real outages. First, measure memory per worker under actual load, not at idle, because a worker running your application code uses far more than a freshly started one. Second, always leave headroom for everything else on the server, and if a database shares the box, reserve memory for it before you do any of this math. The mistake that takes sites down is treating total memory as if it all belongs to PHP.

3. Tune the Dynamic Mode Settings

If you chose dynamic, three more settings control how the pool breathes, and they only apply in that mode. Getting them wrong does not crash the server, but it does waste memory or add latency, so they are worth setting deliberately rather than leaving at the defaults.

pm.start_servers sets how many workers exist right after a restart. Size it to your normal baseline traffic so the server is ready for typical load the moment it comes up.

pm.min_spare_servers and pm.max_spare_servers define the cushion of idle workers the manager keeps ready. When idle workers drop below the minimum, it spawns more. When they climb above the maximum, it retires some. That spare cushion is what absorbs a sudden burst without the delay of cold-starting a worker for every new request.

A reasonable starting point for the example server above looks like this:

pm.start_servers = 20
pm.min_spare_servers = 10
pm.max_spare_servers = 40

The relationship matters more than the exact numbers. Keep start_servers somewhere between the min and max spare values, and set the spare range wide enough to soak up your normal traffic swings without constantly spawning and killing processes. Churning workers up and down wastes processor time, so a slightly generous spare pool is usually cheaper than a stingy one.

4. Recycle Workers With pm.max_requests

Long-running PHP workers have a quiet problem: memory leaks. Not always in your own code, but in extensions and libraries that hold onto a little memory with each request. Over thousands of requests, a worker that started at 40 megabytes can bloat well past that, and bloated workers throw off the careful memory math from tip two.

The fix is to retire each worker after it has handled a set number of requests, which forces it to restart fresh and release everything it was holding:

pm.max_requests = 500

After 500 requests, the worker exits cleanly and the master spawns a replacement. The number is a balance. Set it too low and you spend processor time constantly recycling workers. Set it too high and leaks have more room to grow between restarts. Somewhere in the range of 500 to 1000 suits most applications, and if you watch worker memory creep upward over time, lower the number rather than raising your memory budget.

5. Set Timeouts to Catch Stuck Requests

A worker tied up by a single slow request is a worker that cannot serve anyone else. One database query that hangs, one external API call with no timeout, and that worker sits frozen, holding its memory and its slot in the pool. Enough of those at once and the whole pool locks up while the server still looks healthy on paper.

request_terminate_timeout is the safety valve. It kills any request that runs longer than the limit, freeing the worker to get back to useful work:

request_terminate_timeout = 30s

Set this slightly above the longest request your application legitimately needs. If a normal page renders in two seconds but a heavy report takes twenty, set the limit at thirty so the report survives but a genuinely stuck request gets cut off. Be careful not to set it shorter than your real workloads, or you will start killing valid long-running requests, which is its own confusing kind of failure. Coordinate this with PHP’s own max_execution_time so the two limits do not contradict each other.

6. Enable and Read the Status Page

You cannot tune a pool you cannot see, so turn on the built-in status page. It reports live data on how the pool is behaving, including how many workers are active, how many are idle, and, most usefully, whether requests are queuing because every worker is busy.

Enable it in the pool configuration:

pm.status_path = /status

Then expose that path through your web server and read it under real load. The number to watch is the listen queue, which counts requests waiting because no worker was free to take them. A listen queue that climbs above zero during normal traffic is the clearest possible signal that pm.max_children is too low and you have memory to spare for more workers. If the queue stays at zero and memory is comfortable, your pool is sized well. This one page turns pool tuning from guesswork into something you can actually measure and adjust with confidence.

Conclusion

Tuning PHP-FPM comes down to a chain of decisions that build on one idea: each worker serves one request and costs memory, so the art is fitting as many workers as your memory allows and shaping the pool to match real traffic. You pick a process manager mode that suits your load, calculate pm.max_children from measured memory rather than guessing, tune the dynamic cushion so bursts do not stall, recycle workers to contain leaks, set timeouts so one stuck request cannot freeze the pool, and watch the status page to confirm the numbers are right.

Change one setting at a time and watch the status page and your memory usage after each one, so you always know which adjustment did what. A pool tuned this way uses your hardware fully without ever putting it at risk, which is the whole point.

The fastest way to start is to run the memory-per-worker command on your own server right now, do the division, and compare the answer to whatever pm.max_children is set to today. That one number usually reveals how much performance your server has been leaving on the table.

Related guides

Account services are unavailable until the production backend is configured.