Introduction
You check the server. CPU’s fine. RAM has headroom. Disk isn’t even close to full. And yet the site is crawling. If that sounds familiar, the problem is very likely PHP-FPM.
PHP-FPM sits between your web server and your application, running the PHP code that actually generates your pages. Most people never think about it until something’s wrong, because when it’s configured correctly, it’s invisible. When it’s not, response times climb even though every dashboard you check looks perfectly healthy.
Short for PHP FastCGI Process Manager, it manages a pool of worker processes. Apache or Nginx passes requests to it, those workers execute the scripts, and the output goes back out to the visitor. Get the pool sized right and you’ll notice it on WordPress sites, Joomla installs, custom applications, pretty much anything running PHP.
What follows is a look at how the process actually works, what tends to break it, and a way to tune it on a live server that won’t leave you scrambling to undo a change at 2am.
How PHP-FPM Actually Works
The request path, roughly:
| Browser → Web Server → PHP-FPM → PHP Application → Database/Backend → Response |
A request comes in and gets handed to whatever worker isn’t busy. That worker runs the script and sends the result back out. Fine, as long as there’s a free worker. Once every worker is occupied, though, requests start queuing, and that queue is where a huge chunk of unexplained slowness actually comes from. Nobody notices it in a CPU graph because the processor isn’t doing anything wrong it’s just waiting its turn.
What Usually Causes Poor PHP Performance
Some of these show up constantly:
- Too few workers configured
- Too many workers, quietly chewing through RAM
- A script that’s just slow
- Database queries with no index behind them
- WordPress plugins that were never written with performance in mind
- API calls to some third party that takes forever to respond
- Memory limits set way too high or way too low
- OPcache switched off, or barely configured
- Genuinely heavy server load
A support ticket that comes up a lot: “Site takes five seconds to load, CPU never breaks 30%.” That mismatch is the tell. Load time high, CPU low that combination points at the worker pool and the slow log, not at needing a bigger server.
Checking PHP-FPM’s Current State
Before touching a config file, look at what’s actually happening.
Is the service even up?
systemctl status php-fpm
Service name varies by PHP version could be php8.1-fpm, could just be php-fpm.
What’s running right now:
ps aux | grep php-fpm
Memory overall:
free -m
And live activity:
top
That’s usually enough to tell you if PHP-FPM is the actual bottleneck or if you’re about to waste an afternoon tuning the wrong thing.
Tuning Process Management
Worker behavior comes down to the pm directive. A typical starting point:
pm = dynamic
pm.max_children = 20
pm.start_servers = 5
pm.min_spare_servers = 5
pm.max_spare_servers = 10
pm.max_children caps how many workers can run at once. pm.start_servers is how many spin up at boot. The min/max spare settings control how many idle workers PHP-FPM tries to keep sitting around, ready to go.
Two other modes exist. static keeps a fixed worker count with no scaling at all. ondemand only spins workers up once a request actually shows up, which saves memory but adds a bit of latency on the first hit.
For most sites with steady, predictable traffic, dynamic is the one that works. It scales with demand instead of sitting idle or falling over under a spike.
Getting pm.max_children Right
This is the ceiling on how many requests can run at the same time. Raising it helps when workers are constantly maxed out but only if you actually have the RAM to back it up.
Say a worker eats roughly 150 MB. Then:
150 MB × 20 workers ≈ 3 GB
Rough math, not gospel. Real memory use depends on your app, so check it directly:
ps aux | grep php-fpm
A 4 GB box with pm.max_children set too high will start swapping, and swapping is slower than the problem you were trying to fix in the first place. Base the number on your actual RAM and observed worker size. Not a number from some five-year-old forum post.
PHP Memory Limits
This is different from FPM’s worker settings memory_limit controls how much memory a single script can use.
Check the current value:
php -i | grep memory_limit
Something like:
memory_limit = 256M
Too low, and bigger requests start erroring out. Too high “just to be safe” and you’ve locked up memory that could’ve gone to another worker instead. You want enough room for the app to breathe without giving any one request a blank check.
Using the Slow Log to Find the Real Problem
If requests are genuinely slow, not just stuck in a queue behind other requests, the slow log is where you look.
request_slowlog_timeout = 5s
slowlog = /var/log/php-fpm/www-slow.log
Tail it:
tail -f /var/log/php-fpm/www-slow.log
A common one: WordPress site, five second response time, CPU and RAM both look totally normal. But the slow log keeps flagging the same script, several seconds every single time. That’s not a server problem. Usually it’s a plugin that was never optimized, a query with no index, a slow third-party API, or just plain bad code somewhere in the request path.
This is what separates throwing more hardware at a problem from actually fixing it. The log points at the application. Not the box it’s running on.
OPcache: Don’t Skip This One
OPcache keeps compiled PHP bytecode in memory so scripts don’t get recompiled on every request. Skip it and you’re leaving performance on the table for no reason at all.
Check if it’s on:
php -m | grep -i opcache
Check the config:
php -i | grep -i opcache
A decent starting point:
opcache.enable=1
opcache.memory_consumption=128
opcache.max_accelerated_files=10000
None of these numbers are fixed. Adjust based on PHP version, app size, and how much spare memory you actually have.
Keeping an Eye on It Going Forward
Tuning isn’t something you finish. After any change, keep watching:
systemctl status php-fpm
journalctl -u php-fpm
free -m
top
Turn on the PHP-FPM status page too it’ll show active workers, idle workers, total requests handled, and whether you keep slamming into pm.max_children. If you’re hitting that ceiling repeatedly, the pool probably needs adjusting. Just check your memory headroom first before you bump it again.
Measuring Whether It Actually Worked
Don’t guess. Measure:
curl -o /dev/null -s -w “TTFB: %{time_starttransfer}s\nTotal: %{time_total}s\n” https://example.com/
A real before-and-after:
Before:
TTFB: 2.80s
Total: 3.20s
After tuning PHP-FPM and fixing the slow script:
TTFB: 0.65s
Total: 1.05s
That’s the kind of number that proves something actually changed, instead of just feeling faster.
Wrapping Up
Optimizing PHP-FPM isn’t about cranking the worker count up and hoping for the best. It comes down to matching the config to what the server can actually handle, how much traffic is coming in, and how the app behaves once real load hits it.
When a PHP site is dragging, work through it in order: FPM processes first, then memory, then the slow log, then OPcache, and only then the application code. Measure before you touch anything, and measure again after. Otherwise you’re just guessing whether you fixed the problem or moved it somewhere else.
Do all that carefully tuning, application-level debugging, ongoing monitoring and it adds up to a meaningfully faster site, without needing to throw more server at it.

