LEMP Field Notes
Nginx & PHP

PHP-FPM Tuning for Nginx: Pools and Worker Limits

PHP-FPM Tuning for Nginx: Pools and Worker Limits
In briefPHP-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, database, and caches; divide the remaining FPM budget by a conservative measured worker footprint; then validate 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.

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:

  1. Observe non-FPM memory use during representative peak traffic.
  2. Leave operating headroom rather than assigning every remaining byte.
  3. Measure worker memory while representative routes, plugins, and data sets are active.
  4. Use a conservative worker figure, not the smallest idle process you can find.
  5. Calculate floor(FPM memory budget / measured worker footprint) as a memory ceiling.
  6. 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

An independent publication. Not affiliated with any prior owner of this domain.

FAQ

What does pm.max_children control?

pm.max_children limits the simultaneous requests a PHP-FPM pool can serve. In static mode it is the fixed child count; in dynamic and ondemand modes it is the maximum. Raising it can reduce queueing only when the host and its dependencies have capacity. It does not make an individual PHP request execute faster.

How do I calculate pm.max_children?

Measure non-FPM memory during representative peak traffic, leave operating headroom, and measure a conservative PHP worker footprint under real application routes. Divide the memory budget available to FPM by that worker figure, round down, and then reduce the ceiling if CPU, database connections, storage, or downstream services impose a lower limit. Do not use PHP memory_limit as worker size.

Should PHP-FPM use static, dynamic, or ondemand?

Use the traffic pattern and resource budget. Static keeps a fixed worker count. Dynamic maintains a changing pool between configured spare-process and maximum limits. Ondemand starts workers when requests arrive and removes idle workers after its timeout. Dynamic is often a practical baseline for sustained variable traffic; ondemand can suit quiet pools. Measure startup latency and memory on the actual application.

Should I set pm.max_requests?

Not as a generic performance tweak. PHP documents a default of zero and says a finite value can work around memory leaks in third-party libraries by respawning children. Consider it only when monitoring shows worker memory growing with request count and recycling resets that growth. The underlying extension or application leak still needs to be identified and fixed.

Should Nginx connect to PHP-FPM through a Unix socket or TCP?

When Nginx and PHP-FPM share a host, PHP's manual says a Unix socket should be preferred, with access controlled by socket owner, group, and mode. TCP is useful across hosts or container boundaries but must be explicitly restricted. An exposed FastCGI endpoint permits arbitrary code execution. In either case, FPM's listen value and Nginx's fastcgi_pass must match.

How can I tell whether a PHP-FPM pool is saturated?

Use the restricted FPM status page and monitoring history. A sustained listen queue, repeated max-children events, and rising latency indicate that requests are waiting for workers. Check memory, swap, CPU, database connections, storage, and downstream latency at the same time. A busy counter alone does not prove that raising pm.max_children is safe.