CLOUD
development

PHP-FPM Performance Tuning: pm.max_children and Memory, by the Numbers

CLOUD|September 16, 2026|7 min read|147 views
PHP-FPM Performance Tuning: pm.max_children and Memory, by the Numbers

Most 502 and 504 errors on PHP servers trace back to a wrong pm.max_children value, either too high and starving RAM or too low and queuing requests. The math is simple: divide available memory by average process size, then match the process manager to your traffic.

If your PHP application throws 502 Bad Gateway or 504 Gateway Timeout under load, the most common cause is a pm.max_children value that does not match your server's real memory. The working formula is one line: pm.max_children = (RAM available to PHP) / (average PHP process size). Available RAM is total memory minus what you reserve for the OS, the database, and the web server. A modern Laravel or WordPress process typically uses 40 to 100 MB, so on a نمو plan (4 GB RAM) where you reserve 1.5 GB for everything else, you have 2.5 GB, which is 2560 MB, and dividing by 60 MB per process gives roughly 42 workers as a safe ceiling.

The second rule: pick the process manager that fits your traffic. Use static for steady high traffic, dynamic for the general case, and ondemand for low-traffic sites or when you host several sites on one server. A 502 usually means FPM crashed or hit the worker ceiling, while a 504 means the script exceeded its execution timeout. The fix is never to raise numbers blindly, it is to compute them from memory, watch the FPM log, then adjust on real data. The sections below break down the math, the modes, the monitoring tools, and the common mistakes with real numbers on Cloud plans.

Why pm.max_children is the number that matters

Every PHP request is served by a separate worker that consumes memory. pm.max_children is the maximum number of workers running at once, so it is both your concurrency ceiling and your memory ceiling. Set it too high and, at peak, FPM spawns more workers than RAM can hold, the OOM Killer steps in and kills random processes, and you get a 502. Set it too low and incoming requests beyond that count sit in a queue, wait time climbs, and the request eventually dies with a 502 or 504 and a log line reading server reached pm.max_children.

The first step before any math: measure your real process size on your own server, not a number from an article. Run this during actual traffic to get the average FPM worker memory in MB:

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

If it reports 55 MB, use it with a small safety margin (60 MB). The gap between a lean app and a heavy one is large: WordPress with heavy plugins can hit 120 MB per process, while a lean Laravel API may run fine on 40 MB. This is why no single magic number fits every server, only a formula you apply to your own case.

The formula, by the numbers, on Cloud plans

Here is the formula applied to Cloud Server plans. We reserve memory for the OS, database, and web server, then divide the rest by an average of 60 MB per process:

PlanRAMReserved (OS+DB)Available to PHPSafe pm.max_children
Asas2 GB1 GB1024 MB~15
Numuw4 GB1.5 GB2560 MB~42
Aamal8 GB2.5 GB5632 MB~90
Muassasa16 GB4 GB12288 MB~200

These are theoretical ceilings. Start about 20% below them, then watch. If your database is heavy (MySQL with a large innodb_buffer_pool eats RAM), raise the reserve and lower max_children. The governing rule: a short queue on computed workers beats a full crash from memory exhaustion. If you need more concurrency than your plan's ceiling, upgrading RAM is cheaper and safer than gambling: adding 2 GB RAM for 600 SAR per year lifts your worker ceiling by about 33 workers at 60 MB each, a difference that turns a server choking at peak into one that rides through it.

static, dynamic, ondemand: which to choose

The three modes control how workers are created and kept alive:

ModeBehaviorBest forMemory
staticFixed number of workers always aliveSteady high traffic, one busy siteHighest
dynamicScales between start_servers and max_spare up to max_childrenGeneral case, variable trafficMedium
ondemandNo workers until a request arrives, killed after idleLow-traffic sites, many sites per serverLowest

With static you set only pm.max_children, and every worker runs from boot, so there is no cold-start delay but memory is always reserved. With dynamic you set pm.start_servers, pm.min_spare_servers, and pm.max_spare_servers alongside max_children, balancing readiness against memory. With ondemand FPM starts with zero workers, forks one per new request, then kills it after pm.process_idle_timeout, which is ideal when you host several apps on one server and do not want each app holding RAM while idle. Its only drawback is a cold-start delay on the first request after idle, acceptable on low-traffic sites.

Tuning dynamic in balance

For a نمو plan (max_children = 42), reasonable starting values are: pm.start_servers = 10, pm.min_spare_servers = 8, pm.max_spare_servers = 20. The rule: start_servers sits between min and max, and max_spare stays around half of max_children so you do not keep a large idle pool eating memory. These are starting values you refine by watching, so if spare workers are always consumed, raise start_servers a little to keep the server ready for peaks.

Monitoring FPM: the status page and slowlog

Do not tune by guessing, tune by continuous measurement. Enable pm.status_path in the pool config to get a live status page showing active and idle workers, queue length, the highest worker count reached (max active processes), and how many times you hit the ceiling (max children reached). If the listen queue keeps growing, your requests are queuing and you need to raise max_children or upgrade RAM. Also enable slowlog with request_slowlog_timeout (say 5 seconds) so FPM records the stack trace of any request exceeding that time, telling you exactly which function slows your app instead of guessing. A single week of monitoring reveals your real traffic pattern: your peak, average request time, and whether the bottleneck is workers, slow code, or the database.

opcache doubles performance at no cost

Before adding any resources, make sure opcache is enabled, since it stores compiled code in memory and saves recompiling every file on every request. Set opcache.memory_consumption to at least 128 MB for a medium app, and opcache.max_accelerated_files to a number above your project's file count, since a Laravel project can exceed 10,000 files. Well-tuned opcache lifts throughput noticeably on the same hardware, and it is the cheapest optimization possible because it needs no extra resources.

Common mistakes that cause 502 and 504

  • max_children above RAM capacity: at peak the server collapses with OOM and returns 502. Check dmesg for out-of-memory kill messages.
  • max_children too low: the FPM log shows server reached pm.max_children, requests queue and drop. The fix is to compute it from memory, not from fear.
  • Timeout mismatch: if FPM's request_terminate_timeout is shorter than Nginx's fastcgi_read_timeout, slow scripts return 504. Keep the Nginx timeout slightly longer than the FPM one.
  • Cumulative memory leak: some apps grow their processes over time. Set pm.max_requests = 500 so a worker recycles after that many requests and frees leaked memory.
  • socket vs TCP: use a Unix socket between Nginx and FPM on the same server, faster and lighter than TCP on 127.0.0.1.
  • One pool for all sites: isolate each site in a separate pool with a separate user, so one site hitting its ceiling does not drag the rest down.

Practical steps to tune it right

To tune PHP-FPM on your server with confidence, follow this order:

  • Measure the real average FPM process size with the command above during live traffic.
  • Compute max_children = (RAM minus reserved) / average process, then subtract 20% as margin.
  • Pick the mode: static for one busy site, dynamic for the general case, ondemand for many sites.
  • Set pm.max_requests between 300 and 500 to resist memory leaks, and enable opcache with enough memory.
  • Align the Nginx timeout with FPM's request_terminate_timeout.
  • Enable the status page and slowlog, and watch for a week.
  • If server reached pm.max_children repeats without RAM exhaustion, raise the count a little or upgrade RAM.

Correct tuning turns a server that stumbles at 30 concurrent visitors into one that serves hundreds of requests steadily on the same hardware. Start with the math, not a guess, monitor with tools, not intuition, and if you need more concurrency than your plan holds, adding RAM on Cloud Server costs less than a single night of outages.

Share