Hosting & Servers

Configuring PHP-FPM for a High-Traffic Site: A Practical Guide

If your site returns 502 under heavy load or server memory fills up, the problem is usually in pm.max_children and the dynamic mode. Here you'll see the correct configuration step by step.

Hosting & Servers

The site comes up, works for a few minutes, then suddenly half the requests come back with 502 Bad Gateway and the nginx log keeps repeating the line connect() to unix:/run/php/php8.2-fpm.sock failed (11: Resource temporarily unavailable). If you've seen this, you can be almost certain the problem isn't your code. The problem is that the PHP-FPM pool has consumed all its workers and the request queue is full.

PHP-FPM is a process manager: a number of workers are pre-created or created on the fly, each worker executes only one PHP request at a time, and the rest of the requests wait in the queue. When the queue fills up, nginx has nowhere left to connect and returns that same 502. The whole correct configuration comes down to answering one question: how many workers can I have simultaneously without running out of server memory?

Calculate pm.max_children from memory, not from gut feeling

The number pm.max_children is the cap on simultaneous workers. If you set it larger than the server's actual capacity, instead of a 502 you'll get a worse error: the Linux OOM Killer will kill one of the processes and you may lose MySQL.

First, measure the actual size of a worker. Create a simple script that just does sleep(30), send several simultaneous requests to it, and then:

ps -o rss= -C php-fpm8.2 | awk '{s+=$1; n++} END {print s/n/1024 " MB avg, " n " workers"}'

The typical output for a WordPress site with a few heavy plugins is between 60 and 120 megabytes per worker. Now the math is simple. On a server with 4 gigabytes of RAM where 1.5 gigabytes goes to MySQL, cache, and the operating system, about 2.5 gigabytes remains for PHP. With an 80-megabyte worker, the logical cap is around 30 workers, not 100.

One point many people miss: don't calculate this number on an empty server. Workers grow as the opcache fills up and plugins load. If after a week of uptime each worker's consumption has increased by 30 percent, your cap isn't realistic.

dynamic or ondemand: which should I choose?

The dynamic mode keeps a number of ready workers and fluctuates between pm.start_servers and pm.max_spare_servers. The ondemand mode keeps no ready workers and only creates a process when a request arrives.

Criteriondynamicondemand
First request latencyLower, worker is readyHigher, process creation takes 20 to 60 milliseconds
Memory when idleConsumed as much as start_serversAlmost zero
Peaky trafficSuitableSuitable if pm.max_children is sufficient
Steady, high trafficSuitableFrequent process creation, CPU overhead

My choice for a high-traffic site is dynamic. The reason is simple: on a site that receives several requests per second, the cost of creating a process is higher than the cost of keeping a few ready workers. I choose ondemand when the server hosts several low-traffic sites and I want to free up memory for the database. If you're working on a shared Linux hosting or a small VPS, this decision makes a noticeable difference in RAM consumption.

A balanced configuration for a high-traffic site on a 4-gigabyte server:

pm = dynamic
pm.max_children = 30
pm.start_servers = 8
pm.min_spare_servers = 6
pm.max_spare_servers = 12
pm.max_requests = 500
request_terminate_timeout = 60s

pm.max_requests = 500 retires a worker after 500 requests. This curbs memory leaks from troublesome plugins, and its cost is a bit of overhead from recreating processes. If your plugins are clean, you can raise this to 2000 or 0.

Where to spot the signs of worker shortage

A 502 is only one of the signs. If your site's TTFB goes from 200 milliseconds to 3 seconds under heavy load but the server's CPU isn't above 40 percent, the requests are probably waiting in the PHP-FPM queue rather than the processor being busy. To see the pool's real status:

curl -s http://127.0.0.1/status?full | grep -E "active|idle|listen queue|max children"

If listen queue shows a number other than zero, it means requests are stuck behind the queue. If max children reached is greater than zero, you've hit the worker cap. You should measure these two numbers during peak periods, not at 3 in the morning.

Here's where people go wrong: many see the queue and immediately double pm.max_children. The result is that a few hours later the server faces OOM, MySQL restarts, and the site goes completely down. A full queue isn't a sign of worker shortage; it's a sign that each worker is taking too long. You first need to understand why a request takes 2 seconds. If it's a slow query or an HTTP request to an external API, adding workers only increases the number of waiting processes.

To find slow requests, enable slowlog:

request_slowlog_timeout = 5s
slowlog = /var/log/php8.2-fpm-slow.log

After a few hours, open the file and see which function is stuck in the call stack. In most cases you'll find a file_get_contents to an external service or a query without an index. For definitively root-causing sporadic outages, also see the method for root-causing intermittent outages; the pattern in the logs usually shows exactly what has remained hidden in FPM.

Side settings that affect stability

Take opcache seriously

Without opcache, each worker reads and compiles PHP files from disk on every request. With opcache.memory_consumption = 256 and opcache.max_accelerated_files = 20000, each request's execution time drops noticeably, and as a result workers are freed up faster. This means that same cap of 30 workers handles more load.

Socket or TCP?

A Unix socket on the same server is slightly faster and doesn't involve the filesystem. If nginx and PHP are on the same machine, choose the socket. If PHP is on another server, you have no choice but TCP, and you must restrict listen = 0.0.0.0:9000 with a firewall; otherwise port 9000 stays open to the internet, and this is a common security mistake.

Align the timeouts

If request_terminate_timeout in FPM is 60 seconds but fastcgi_read_timeout in nginx is 30, the user gets a 504 instead of a clear error. Align these three values: PHP's own timeout, the FPM value, and the nginx value. For when you get a 504 and aren't sure which layer is at fault, the guide to fixing the 504 error shows the order of investigation.

Take a baseline before changing anything

Any change to this file without measurement is a guess. Before touching php-fpm.conf, record RAM consumption, the number of active workers, and TTFB in three periods (morning, peak, night). Then change one parameter and wait 24 hours. Changing five parameters at once means you'll never know which one was effective.

If your current server still falls short at peak even after these settings, the issue is no longer configuration; it's capacity. At that stage, a dedicated server or a plan with more RAM is a more logical choice than blindly raising pm.max_children. For quickly monitoring your site's responsiveness, you can also use the free webmaster tools to see what effect the changes actually had.

Frequently asked questions

What number should I set pm.max_children to?

There's no fixed number. Measure a worker's actual consumption with ps -o rss=, subtract the memory that goes to the database and operating system, and divide the remainder by each worker's consumption. To be safe, leave a 10 to 20 percent margin.

Why does my site return 502 despite having free RAM?

The PHP-FPM request queue is probably full, not that RAM has run out. Check the listen queue value by enabling the status page. If it's anything other than zero, the problem is each request being slow, not a worker shortage.

How much does the difference between dynamic and ondemand matter in practice?

On a high-traffic site with steady traffic, dynamic gives lower latency because a ready worker always exists. On a server hosting several low-traffic sites, ondemand frees up more memory and is worth it. The difference during peak traffic can be a few hundred milliseconds in TTFB.

What should I do after changing the settings?

Load the settings without dropping active connections using systemctl reload php8.2-fpm. Then monitor the slow log and the max children reached value for 24 hours. If the number stays at zero and TTFB drops, the configuration was correct.

ServerNet Support

ServerNet engineering & editorial team — specialists in infrastructure, networking and web hosting.

Linux Hosting
Share:

Comments 0

No comments yet — be the first!

Leave a comment

Related service

Linux Hosting

PHP & MySQL hosting on NVMe RAID-10 with LiteSpeed — the solid base for any website, from personal blogs to enterprise Laravel apps. At a price competitors can't explain.