php-fpm max_children-waarschuwing: zoek de oorzaak in de logs voordat je workers bijzet

De waarschuwing php-fpm max_children is een teken dat alle PHP-workerprocessen bezig zijn met verzoeken, zodat nieuwe verzoeken in de wachtrij komen. Op een WordPress-server met Nginx, Apache en php-fpm waar deze waarschuwing al weken opliep, heb ik met de logs de verzoeken aangewezen die de workers vulden, en het opgelost met een rate limit in nginx in plaats van de instelling te verhogen.
Bij de wekelijkse controle werd de waarschuwing 27–187 keer per dag gelogd, maar in dezelfde vier dagen kregen bezoekers nul 502- of 504-fouten. Wat de workers vulde, waren geen bezoekers maar een bot die beheer-URL's bestookte en een scanner die paden opvroeg die niet bestaan.
1️⃣ php-fpm max_children & seems busy
Als de waarschuwing oploopt tot 66 per dag, lees je dat makkelijk als 66 keer te weinig workers. De php-fpm-log mengt twee verschillende waarschuwingen, en de meeste zijn vroege waarschuwingen die worden gelogd voordat de limiet is bereikt.
[09-Oct-2026 00:16:18] WARNING: [pool www] seems busy (you may need to increase pm.start_servers, or pm.min/max_spare_servers), spawning 8 children, there are 0 idle, and 15 total children
[09-Oct-2026 13:02:05] WARNING: [pool www] server reached pm.max_children setting (26), consider raising it[seems busy] betekent dat er 0 workers vrij zijn en er een nieuw proces wordt aangemaakt; met procesbeheer op dynamic wordt dit al bij een korte stijging van het aantal verzoeken gelogd. [server reached pm.max_children] is het moment waarop de limiet van 26 werd bereikt en er geen meer konden worden aangemaakt, en vanaf dan wachten nieuwe verzoeken.
Het is het verschil tussen nog een ober erbij roepen en gasten buiten laten staan omdat elke stoel bezet is.
Eén dag per type tellen gaf 58 keer seems busy en 6 keer max_children. De controleroutine telde beide op en meldde 66, maar de limiet werd in werkelijkheid zes keer bereikt.
grep '09-Oct-2026' /var/log/php-fpm/error.log | grep 'pool www' | grep -c 'seems busy'58grep '09-Oct-2026' /var/log/php-fpm/error.log | grep 'max_children' | cut -c2-2109-Oct-2026 13:02:05
09-Oct-2026 13:52:10
09-Oct-2026 18:09:20
09-Oct-2026 18:42:17
09-Oct-2026 18:46:07
09-Oct-2026 19:12:44De zes tijdstippen die het tweede commando toont, worden het uitgangspunt om de oorzaak te traceren; de volgende stap gebruikt alleen deze tijdstippen.
2️⃣ Waarom max_children verhogen niet de oplossing is
De limiet van 26 verhogen kan het aantal waarschuwingen verlagen, maar verandert niets aan welke verzoeken de workers vulden.
De gebruikelijke berekening deelt het beschikbare geheugen door het gemiddelde geheugen van één worker. Op de gemeten server gebruikten 26 workers gemiddeld 131MB en maximaal 214MB, en met het resterende beschikbare geheugen was er op papier nog ruimte voor meer workers.
ps -o rss= -u www -C php-fpm | awk '{s+=$1; n++; if($1>m) m=$1} END{print n, s/n/1024, m/1024, s/1024}'26 130.579 214.27 3395.05free -mDe vier waarden in de eerste uitvoer zijn het aantal workers, het gemiddelde geheugen (MB), het maximale geheugen (MB) en het totaal (MB); de kolom available in de uitvoer van free is het beschikbare geheugen.
Zijn er meer workers dan CPU-cores, dan verhogen extra workers het aantal gelijktijdig afgehandelde verzoeken niet; ze maken alleen elk verzoek langer. Zolang bezoekers nul 502- of 504-fouten krijgen, levert het verhogen van de limiet niets op en krijgen de bots en scanners die de workers bezetten alleen meer processen.
Controleer voordat je de limiet verhoogt of de verzoeken die op de waarschuwingsmomenten workers bezetten van bezoekers kwamen of niet; het volgende deel laat zien hoe je dat in access.log nagaat.
3️⃣ De oorzaak traceren in access.log: tijd, IP, pad, statuscode
Kies één waarschuwingsmoment en het antwoord staat in de access.log van die 1–2 minuten. De volgorde is: de regels op tijd uitfilteren, per IP tellen en dan de paden en statuscodes van de bovenste IP's bekijken.
grep '09/Oct/2026:18:09' /var/log/nginx/access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head 115 140.245.x.x
61 187.15.x.x
17 3.36.x.x
8 91.239.x.x
4 79.127.x.x
3 109.248.x.x
3 109.248.x.x
3 109.248.x.x
2 79.127.x.x
2 40.77.x.xgrep '09/Oct/2026:18:09' /var/log/nginx/access.log | awk '{print $7, $9}' | sort | uniq -c | sort -rn | head 55 / 200
13 /wp-login.php 404
13 /wp-admin/plugins.php 302
13 /wp-admin/index.php 302
13 /wp-admin/edit.php 302
12 /wp-admin/profile.php 302
4 / 499
3 /xmlrpc.php 302
2 /xmlrpc.php 499
2 /wp-content/themes/kinsta/images/duos/small/wheelchair.svg 404Op 9 oktober om 18:09 werden in één minuut 286 verzoeken gelogd; één IP was goed voor 115 daarvan, de paden waren beheer-URL's zoals /wp-admin/profile.php en elke statuscode was 302.
De statuscode vertelt of PHP werd bereikt. Een 200 voor gecachete HTML die nginx direct serveert, en 444 of 400, raken PHP nooit, terwijl 302, 404 en een niet-gecachete 200 elk een worker gebruiken. Vraagt een uitgelogde bezoeker een beheer-URL op, dan antwoordt WordPress met een 302 naar de loginpagina, en die ene 302 is één PHP-uitvoering.
Draait één server twee of meer sites, dan legt de globale access.log regels van allebei vast. Eerst dacht ik dat deze bot de eerste site aanviel, maar vergelijken met de eigen log van elke site liet zien dat 22.390 van de 22.391 verzoeken voor de tweede site waren.
4️⃣ Drie soorten verzoeken die de workers vulden: wp-admin van een bot, 404's van een scanner, cache-preload
Toen ik dezelfde volgorde op elk waarschuwingsmoment toepaste, bleken drie soorten verzoeken de workers te vullen.
De eerste was een bot die steeds beheer-URL's opvroeg. Eén IP stuurde in drie uur 22.391 verzoeken; 9.944 daarvan gingen naar vier paden, waaronder /wp-admin/profile.php, en omdat elke 302 voor een uitgelogde bezoeker één PHP-uitvoering is, gebruikte hij 9.944 keer een worker.
De tweede was een kwetsbaarheidsscanner die paden opvroeg die niet bestaan. Ook met Googlebot in de User-Agent is het geen Googlebot: het IP dat 14.672 verzoeken per dag stuurde, was een virtuele server in Google Cloud waarvan de reverse DNS eindigt op googleusercontent.com, terwijl Googlebot eindigt op googlebot.com. Hetzelfde IP wisselde af tussen bingbot, Applebot, GPTBot en ClaudeBot.
Verzoeken naar bestanden als .env worden door nginx in 0,04 seconde met een 444 afgekapt, maar gewone paden die niet onder de blokkeerregels vallen, zoals /userfiles of /secrets.yml, laten WordPress een 404-pagina genereren. Die 404-pagina kost 266KB en 1,4–2,9 seconden, net zoveel als een normaal bericht genereren, en één IP vroeg hem 4.069 keer per dag op.
De derde was de server zelf. Een bericht of vertaling opslaan leegt de cache, waarna de preload van WP Rocket alle URL's opnieuw opvraagt; dat kwam op 8.489 verzoeken per dag en tot 60 per minuut. Ook de controles van de indexstatus door Rank Math gebruikten via Action Scheduler 754 keer per dag een worker. Beide concentreerden zich op werkuren en veroorzaakten geen fouten bij bezoekers, dus heb ik ze buiten de oplossing gelaten.
In de simulator hieronder zie je op welke laag elk verzoek eindigt en of het een PHP-worker gebruikt.
| Verzoek | Stopt bij | Antwoord | Workers |
|---|---|---|---|
| wp-admin van bot | WordPress | 302 (naar de loginpagina) | Gebruikt 1 |
| Scanner .env-type | Blokkeerregel van nginx | 444 (0,04 s) | Niet gebruikt |
| Scanner gewoon nep-pad | WordPress | 404-pagina (266KB · 1,4–2,9 s) | Gebruikt 1 |
| Cache-preload | WordPress | 200 · opgeslagen als cachebestand | Gebruikt 1 |
| Bezoek aan een gecachet bericht | Cachebestand | 200 (nginx antwoordt direct) | Niet gebruikt |
| Bezoek aan een niet-gecachet bericht | WordPress | 200 · opgeslagen als cachebestand | Gebruikt 1 |
5️⃣ Rate limiting in nginx: verzoeken van uitgelogde bezoekers beperken en het echte IP herstellen
Ik koos voor rate limiting in plaats van blokkeren. Blokkeer je een pad, dan stapt de scanner over op andere paden; blokkeer je een IP, dan duikt de volgende dag een ander IP op. Alleen verzoeken van uitgelogde bezoekers die PHP bereiken, worden beperkt, tot 30 per minuut en 60 tegelijk per IP, en het overschot krijgt een 429.
Voor een server achter een CDN is deze instelling alleen niet genoeg. Het IP dat nginx ziet, is het adres van de CDN-edge, dus alle bezoekers die via die edge komen, worden op één IP samengevoegd; je moet eerst de lijst met edge-IP's registreren met set_real_ip_from om het echte IP uit X-Forwarded-For te herstellen. Verzoeken met een logincookie zijn vrijgesteld, maar er wordt niets vrijgesteld op basis van User-Agent.
# http-context
real_ip_header X-Forwarded-For;
real_ip_recursive on;
set_real_ip_from 203.0.113.0/24; # lijst met CDN-edges
map $http_cookie $php_anon_key {
default $binary_remote_addr;
~*wordpress_logged_in '';
}
limit_req_zone $php_anon_key zone=php_anon:10m rate=30r/m;
# location die naar PHP doorgeeft
location @apache {
limit_req zone=php_anon burst=60 nodelay;
limit_req_status 429;
proxy_pass http://127.0.0.1:8080;
}Gecachete pagina's worden binnen nginx door try_files beantwoord en bereiken deze location nooit, en admin-ajax.php is een aparte location, dus valt die niet onder de limiet.
Test met gelijktijdige verzoeken. Opeenvolgende verzoeken komen nooit boven de 30 per minuut uit omdat één 404 1,5 seconde duurt, dus ze raken de limiet nooit; 100 verzoeken met 20 tegelijk geven 67 keer 404 en 33 keer 429, wat bevestigt dat de limiet werkt.
seq 1 100 | xargs -P 20 -I{} curl -s -o /dev/null -w '%{http_code}\n' https://example.com/zz-{} | sort | uniq -cTelt de uitvoer 429's, dan werkt de limiet. Heb je het eigen IP van de server vrijgesteld, hef die vrijstelling dan tijdens het testen even op of test vanaf een ander netwerk.
6️⃣ Voor en na
De limiet op beheer-URL's voor uitgelogde bezoekers ging op de avond van 9 oktober live, en de limiet op verzoeken die PHP bereiken op de avond van de 11e. 302's voor beheer-URL's daalden van 9.996 op de 9e naar 11 op de 10e en 1 op de 11e, en het IP dat in drie uur 22.391 verzoeken stuurde, heeft sinds de volgende dag niets meer gestuurd.
Voor de scanner is de limiet net pas ingesteld, dus er zijn nog geen cijfers over een hele dag; het oordeel komt van hoe ver het aantal 404-pagina's dat WordPress genereert (2.563 op de 9e, 2.612 op de 10e, 1.630 op de 11e) bij de controle van volgende week daalt. Fouten 502 en 504 bij bezoekers waren zowel ervoor als erna 0.
Deze cijfers zijn rechtstreeks geteld uit de logs van een WordPress-server met Nginx, Apache en php-fpm, en het oordeel was niet gebaseerd op het aantal waarschuwingen maar op fouten bij bezoekers en verzoeken die PHP bereiken.
🔢 FAQ & aanbevolen content
Cache
Websitecache & servercache
Legt laag voor laag uit hoe gecachete verzoeken PHP vermijden.
Beveiliging
Beveiligingslekken in wp-config.php
Hoe je het configuratiebestand waar scanners op mikken beschermt met rechten en serverregels.
Linux
15 categorieën Linux-commando's
Een tabel die commando's in 15 categorieën indeelt, waaronder de grep en awk waarmee de logs zijn geteld.
ℹ️ Affiliatemelding
De content van deze site bevat affiliatelinks. Koopt een bezoeker via een van deze links een product of dienst, dan ontvangt deze site een commissie van de verkoper. Het bedrag dat de koper betaalt (bij acties gaat het omlaag ↓) wordt daardoor niet hoger. De vermelde prijzen, kortingen en voorraad gelden op het moment van schrijven en kunnen afwijken; controleer ze daarom vóór je aankoop bij de verkoper. Producten worden gekozen en beoordeeld volgens onze eigen criteria, en commissies hebben geen invloed op de volgorde of de inhoud van de beoordelingen.