PHP-FPM Tuning for Nginx: Pools and Worker Limits

- How should you tune PHP-FPM for Nginx?
- First identify the constraint
- What do static, dynamic, and ondemand mean?
- How do you size pm.max_children?
- When should you use separate pools?
- Which status signals matter?
- What should the slow log tell you?
- How should Nginx connect to the pool?
- Apply changes without guessing
- Sources
How should you tune PHP-FPM for Nginx?
PHP-FPM tuning starts with measurements, not a copied pm.max_children value. Choose static, dynamic, or ondemand for the traffic pattern; reserve memory for the operating system, Nginx, the database, and caches; divide the remaining FPM budget by a conservative measured worker footprint; then validate the result against queue depth, max-children events, latency, CPU, and swap. Keep each pool's listen endpoint aligned with Nginx, and restrict any status endpoint to internal access.
This guide is documentation-based. Its directives were checked against the current PHP and Nginx manuals and Ubuntu 24.04 package records, but the examples were not executed on a clean Ubuntu test host. It therefore contains no copy-and-paste command blocks. Verify the installed versions, packaged configuration, application behavior, and service procedure on the server before changing a live pool.
The reference platform is Ubuntu 24.04 LTS (Noble), whose distribution packages use the PHP 8.3 and Nginx 1.24 series. Security-update suffixes change over time, so the major and minor series are the useful scope here. The Ubuntu package file list places FPM's main configuration at /etc/php/8.3/fpm/php-fpm.conf and its packaged pool configuration at /etc/php/8.3/fpm/pool.d/www.conf. A third-party PHP repository or another Ubuntu release may use different versions and paths.
First identify the constraint
More workers do not make one slow PHP request execute faster. They allow more PHP requests to run concurrently. Raising the ceiling can help when requests are waiting for a free worker and the host has spare resources. It can make the outage worse when the real limit is memory, CPU, database connections, storage latency, or an external API.
Establish a baseline before editing anything. Record the pool's active and idle processes, listen queue, max-children events, slow-request count, response latency, host memory, swap activity, CPU utilization, and database pressure during representative traffic. Also record the current configuration and the installed package versions. A change without a baseline is just a new configuration with better self-esteem.
The joke stops there. On a production host, memory exhaustion and excessive process counts can terminate services or make the machine unresponsive. Change one variable at a time and keep a tested rollback path.
What do static, dynamic, and ondemand mean?
The PHP-FPM configuration manual defines three process-manager modes. Their mechanics are exact; the workload choices below are operational judgments.
| Mode | What PHP documents | Practical trade-off |
|---|---|---|
static |
FPM keeps a fixed number of children equal to pm.max_children |
Predictable process count, but all workers occupy resources even when traffic is quiet |
dynamic |
FPM changes the child count using pm.max_children, pm.start_servers, pm.min_spare_servers, and pm.max_spare_servers |
A reasonable starting mode for sustained traffic that rises and falls |
ondemand |
FPM spawns children when requests arrive and can remove them after pm.process_idle_timeout |
Reduces idle workers on quiet pools, at the cost of process startup after idle periods |
pm.max_children is mandatory in every mode. With static, it is the number of children created. With dynamic and ondemand, it is the maximum. PHP describes it as the limit on simultaneous requests served by that pool.
For dynamic, the spare-server directives are not decorative. pm.min_spare_servers and pm.max_spare_servers are mandatory, and pm.start_servers applies only at startup. PHP documents the default start value as the average of the two spare-server settings. For ondemand, PHP documents a pm.process_idle_timeout default of 10 seconds. These are upstream PHP defaults; the effective Ubuntu pool file may override them.
How do you size pm.max_children?
Do not divide total RAM by PHP's memory_limit. PHP defines memory_limit as the maximum memory a script may allocate. It is not a documented measurement of a worker's resident footprint, and it says nothing about the memory already required by the operating system, Nginx, the database, caches, monitoring agents, or other pools.
Use a measured budget instead:
- Observe non-FPM memory use during representative peak traffic.
- Leave operating headroom rather than assigning every remaining byte.
- Measure worker memory while representative routes, plugins, and data sets are active.
- Use a conservative worker figure, not the smallest idle process you can find.
- Calculate
floor(FPM memory budget / measured worker footprint)as a memory ceiling. - Reduce that ceiling if CPU, database connections, storage, or a downstream service becomes the earlier constraint.
The result is a starting limit, not a certificate of capacity. Validate it under representative traffic. A growing listen queue and a nonzero max children reached counter show that the pool has reached its concurrency limit, but they do not prove that raising the limit is safe. If CPU is saturated or the database is already queuing work, more PHP children add contenders rather than capacity.
With several pools, perform the budget across the host, not once per site. Ten pools cannot each spend the same unallocated RAM. PHP also documents the global process.max directive for controlling the total number of FPM processes when many dynamic pools are used. Its documented default is 0, and the manual says to use it with caution. Do not introduce it merely to compensate for pool limits that were never added together.
When should you use separate pools?
A separate pool can give a site its own Unix user, group, endpoint, process-manager policy, worker ceiling, logs, and status counters. That makes a noisy site easier to observe and prevents it from consuming another pool's reserved workers.
The boundary is only as strong as its operating-system permissions. A separate pool is not a container. If two pool users can read the same application secrets or writable files, the names in square brackets do not create isolation.
Each pool needs a unique listen address. For a Unix socket, set ownership and group so the Nginx worker can connect without making the socket world-writable. PHP documents an upstream Unix-socket mode default of 0660, but packaged and local settings can differ. Read the effective pool configuration rather than copying that value as an Ubuntu guarantee.
Separate pools also carry an idle-memory cost under static or dynamic. ondemand can reduce that cost for a rarely used site, but the first request after an idle period may wait for a child to start. Measure the trade-off on the actual application.
Which status signals matter?
PHP's FPM status page exposes pool-specific evidence: current and maximum listen queue, idle and active processes, total processes, maximum active processes, max children reached, and slow requests. These values reset when FPM restarts, so capture them in monitoring if historical trends matter.
The status page is sensitive. Full output can reveal request URLs, script paths, users, resource use, and other operational details. PHP explicitly says to restrict it to internal requests or known client IP addresses. Do not expose it as an ordinary public site path, rely on an obscure URL, or let a public server block inherit access to it accidentally.
pm.status_path enables the status URI and defaults to unset. PHP 8.3 also supports pm.status_listen, which creates a separate status listener that can answer while the application pool is occupied by long requests. That improves observability during saturation; it does not remove the need for an internal-only access control.
Use the counters together. A temporary busy pool with no queue may be healthy. A sustained queue, repeated max-children events, and rising latency indicate constrained capacity. A low active-process count with slow responses points elsewhere, often to application, database, storage, DNS, or downstream-service latency.
What should the slow log tell you?
request_slowlog_timeout tells FPM when to write a PHP backtrace to the pool's slowlog file. PHP documents its default as 0, meaning off. The slow log helps identify which code path occupied a worker; it is not a request killer.
Do not confuse it with request_terminate_timeout. That separate directive kills a worker after its threshold, and PHP also documents it as off by default. A termination setting can interrupt legitimate imports, reports, uploads, or shutdown work. Set neither timeout from a generic article number. Start from the application's legitimate request profile, enable observation first, and decide with the application owner what may safely be terminated.
pm.max_requests controls how many requests a child executes before it respawns. PHP documents a default of 0, meaning endless request processing, and says a finite value can work around memory leaks in third-party libraries. It is not a general speed control. Consider it when measured worker memory grows with request count and recycling demonstrably resets that growth. The correct long-term fix remains the leaking extension or application code.
How should Nginx connect to the pool?
FPM's listen and Nginx's fastcgi_pass must describe the same endpoint. The Nginx FastCGI module documentation permits an IP address and port or a Unix-domain socket. Our Ubuntu 24.04 LEMP installation guide shows the baseline stack, and the Nginx server-block guide explains where the PHP location belongs.
For Nginx and FPM on the same host, PHP's current manual says a Unix socket should be preferred. It warns that an exposed FastCGI endpoint allows arbitrary code execution. If TCP is required, listen.allowed_clients applies only to TCP listeners; PHP documents it as unset by default, which accepts any address. Do not publish an FPM port to the internet or assume a host firewall is a substitute for a correctly scoped listener.
Do not treat Nginx timeout increases as PHP-FPM tuning. Nginx documents fastcgi_read_timeout as 60 seconds by default and measures it between successive reads, not across the whole response. Raising it may allow a slow request to occupy a PHP worker for longer. Find the reason for the delay before changing the timeout.
Apply changes without guessing
Retain the current configuration, change one pool variable, validate both FPM and Nginx configuration with the tools installed on that host, and use the service unit's supported reload or restart procedure during a monitored window. This article does not provide those commands because they were not tested here on a clean Ubuntu 24.04 host.
After the change, compare the same baseline signals. Keep it only if queueing or latency improves without unsafe memory, swap, CPU, database, or error behavior. If the pool is still slow with spare workers, stop tuning the process count and profile the work inside the request.
That is the unglamorous answer to PHP-FPM tuning: measure the queue, measure the workers, respect every other service on the host, and make the smallest change the evidence supports.
Sources
- PHP Manual, FPM Configuration — accessed September 3, 2026; process-manager semantics, pool directives, documented defaults, slow logging, socket permissions, and FastCGI listener warnings.
- PHP Manual, FPM Status Page — accessed September 3, 2026; status fields, reset scope, separate status listener, and the internal-access warning.
- PHP Manual, Core
memory_limitDirective — accessed September 3, 2026; the per-script allocation scope ofmemory_limit. - Nginx, FastCGI Module — accessed September 3, 2026;
fastcgi_passendpoint forms and FastCGI timeout semantics. - Nginx, Controlling Nginx — accessed September 3, 2026; configuration validation, rollback, and worker behavior during reload.
- Ubuntu,
php8.3-fpmPackage and package file list — accessed September 3, 2026; Noble's PHP-FPM series and packaged configuration paths. - Ubuntu, Nginx Package and Ubuntu 24.04 release notes — accessed September 3, 2026; Noble's Nginx 1.24 series.
An independent publication. Not affiliated with any prior owner of this domain.