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

Affiliatemelding v3.1
Affiliatemelding v3.2 (mobiel)
Testomgeving RHEL-familieAlmaLinux 9.8root-accountGecontroleerd op 12-10-2026

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'
58
grep '09-Oct-2026' /var/log/php-fpm/error.log | grep 'max_children' | cut -c2-21
09-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:44

De 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.05
free -m

De 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.x
grep '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 404

Op 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.

🧭 Simulator van het verzoekpadwp-admin-beheerpaginaUitlegsimulator
🗂️ Verzoeken die de workers vulden
wp-admin-verzoeken van een bot
Vraagt een uitgelogde bezoeker een beheer-URL op, dan antwoordt WordPress met een 302 naar de loginpagina. Die ene 302 is één PHP-uitvoering.
Scanner: bestanden als .env
Verzoeken naar bestanden als .env worden door de blokkeerregel van nginx met een 444 afgekapt. 0,04 seconde, en PHP wordt niet geraakt.
Scanner: gewone nep-paden
Gewone paden die niet onder de blokkeerregels vallen, zoals /userfiles of /secrets.yml, laten WordPress een 404-pagina genereren. 266KB en 1,4–2,9 seconden, net zoveel als een normaal bericht genereren.
Cache-preload
Een bericht of vertaling opslaan leegt de cache, en de preload van WP Rocket vraagt alle URL's opnieuw op. Tot 60 per minuut, en elk verzoek gebruikt één worker.
Bezoek aan een gecachet bericht
Is er een cachebestand, dan antwoordt nginx direct en wordt PHP niet geraakt.
Bezoek aan een niet-gecachet bericht
Is er geen cachebestand, dan genereert PHP de pagina en slaat die HTML op als cachebestand.
Volledige stroom
Een verzoek gaat van boven naar beneden en stopt op de laag waar het antwoord eindigt. Alleen verzoeken die helemaal tot WordPress doorgaan, gebruiken een PHP-worker.
VerzoekStopt bijAntwoordWorkers
wp-admin van botWordPress302 (naar de loginpagina)Gebruikt 1
Scanner .env-typeBlokkeerregel van nginx444 (0,04 s)Niet gebruikt
Scanner gewoon nep-padWordPress404-pagina (266KB · 1,4–2,9 s)Gebruikt 1
Cache-preloadWordPress200 · opgeslagen als cachebestandGebruikt 1
Bezoek aan een gecachet berichtCachebestand200 (nginx antwoordt direct)Niet gebruikt
Bezoek aan een niet-gecachet berichtWordPress200 · opgeslagen als cachebestandGebruikt 1
Een uitlegsimulator die alleen in je browser draait. Antwoorden en tijden komen uit logs en metingen van een server met Nginx + Apache in oktober 2026.

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 -c

Telt 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.

📊Voor en na de rate limitPer dag · logtelling
ErvoorErna
302-antwoorden voor beheer-URL's
302's die WordPress stuurde op wp-admin-verzoeken van uitgelogde bezoekers. De limiet op beheer-URL's voor uitgelogde bezoekers ging op de avond van 9 oktober live.
02,4994,9987,4979,996
10/99,996Ervoor
10/1011Erna
10/111Erna
Verzoeken van het bot-IP
Dagelijkse verzoeken van het ene IP dat in drie uur 22.391 verzoeken stuurde.
05,59811,19616,79322,391
10/922,391Ervoor
10/100Geen verzoeken
10/110Geen verzoeken
404-pagina's gegenereerd door WordPress
Scannerverzoeken naar gewone nep-paden. De limiet op verzoeken die PHP bereiken ging op de avond van 11 oktober live, dus het oordeel volgt bij de controle van volgende week.
06531,3061,9592,612
10/92,563Ervoor
10/102,612Ervoor
10/111,630Ervoor · ingesteld op de avond van de 11e
De cijfers zijn dagelijkse tellingen van de access.log van nginx van 9 t/m 11 oktober 2026.

🔢 FAQ & aanbevolen content

dynamic maakt workers aan en stopt ze naargelang het aantal verzoeken, en logt de waarschuwing seems busy als er 0 workers vrij zijn. static houdt altijd max_children workers aan, dus er is geen vertraging bij het aanmaken en geen waarschuwing, maar het geheugen is altijd bezet. Deze server bleef op dynamic met 26; heb je geheugen over en is het aantal verzoeken de hele dag gelijkmatig, dan past static.

Zet hem aan als bepaalde verzoeken op de waarschuwingsmomenten lang lijken te duren. Stel in de poolconfiguratie het pad [slowlog] en [request_slowlog_timeout] in, dan wordt van elk verzoek dat die tijd overschrijdt het uitvoeringspunt gelogd. In dit geval was het aantal verzoeken de oorzaak, dus volstond het tellen van access.log en is slowlog niet gebruikt.

De limiet geldt alleen voor verzoeken van uitgelogde bezoekers die de cache omzeilen en PHP bereiken, en alleen het overschot krijgt een 429 als één IP boven de 30 per minuut of 60 tegelijk komt. In de telling van 11 oktober was het enige IP boven die grens een scanner, en de overschrijding is binnen een minuut voorbij, dus het volgende verzoek krijgt een normaal antwoord.

Dat heb ik niet gedaan. De preload concentreerde zich op de uren waarin berichten en vertalingen werden opgeslagen en leidde nooit tot fouten bij bezoekers. De maatstaf is niet het aantal waarschuwingen, maar of bezoekers 502- of 504-fouten kregen; is dat aantal 0, dan blijft de preload zoals hij is.

ℹ️ 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.

Geef een reactie

Je e-mailadres wordt niet gepubliceerd. Vereiste velden zijn gemarkeerd met *

Laat zien dat je geen machine bent: 1   +   2   =