Een PHP worker is één PHP-FPM child process dat op een gegeven moment WordPress-code draait voor precies één HTTP-verzoek. Een pool van die workers bedient je hele site. "Workers uitgeput" betekent dat elke worker in de pool nu bezig is en het volgende verzoek moet wachten in een rij tot er eentje vrijkomt. Wordt die rij te lang of duurt het wachten te lang, dan krijgt de bezoeker een trage pagina, een 502 of een 504 te zien.
Wat een PHP worker eigenlijk is
WordPress is PHP. Elk niet-gecachet verzoek aan een WordPress-site moet door een PHP-proces in HTML worden omgezet. Op moderne hosting is dat proces een child van PHP-FPM (FastCGI Process Manager), de PHP-daemon die continu draait en waar de webserver verzoeken aan doorgeeft via een FastCGI-socket. Elk child process is een worker. nginx (of Apache) accepteert het HTTP-verzoek en stuurt het via fastcgi_pass door naar PHP-FPM, dat het toewijst aan een vrije worker. Die worker boot WordPress, draait de queries, bouwt de HTML, geeft het terug en gaat weer in de pool zitten wachten op het volgende verzoek.
Workers zijn georganiseerd in pools. Een pool is gedefinieerd in een FPM-configuratiebestand (meestal /etc/php/8.3/fpm/pool.d/www.conf op Debian-achtige systemen) en heeft een vast maximum aan child processes. Dat plafond heet pm.max_children. Zijn alle pm.max_children workers tegelijk bezig, dan is de pool uitgeput. Er is geen overloop en de pool schaalt niet automatisch boven dat plafond.
Hoe een PHP-FPM pool eigenlijk schaalt
PHP-FPM kent drie process manager-modi en elke modus plant workers anders in. De modus zet je met de pm-directive in de pool-config.
pm = static: de pool draait altijd preciespm.max_childrenworkers, of ze nu bezig zijn of niet. Voorspelbaar en de laagste latency per verzoek. Verspilt geheugen als verkeer pieken kent.pm = dynamic: de pool start metpm.start_serversworkers en schaalt tussenpm.min_spare_serversenpm.max_spare_serversidle workers, metpm.max_childrenals plafond. Veruit de meest gebruikte default. Goed voor gemengde workloads.pm = ondemand: er bestaan geen workers tot er een verzoek binnenkomt. Workers worden on demand gestart en weer opgeruimd napm.process_idle_timeout. De laagste geheugenvoetafdruk maar de hoogste cold-start latency. Veel shared hosting werkt zo, omdat veel sites één server delen.
Welke modus ook draait, pm.max_children blijft het harde plafond. Een verzadigde pool levert ongeacht de modus hetzelfde resultaat op: het volgende verzoek moet wachten.
Wat er met zo'n wachtend verzoek gebeurt, hangt af van de listen backlog. PHP-FPM houdt binnenkomende verzoeken vast in een kernel socket queue (gedimensioneerd via listen.backlog), terwijl nginx of Apache de verbinding intussen openhoudt. Komt er een worker vrij voordat nginx zijn fastcgi_read_timeout haalt, dan wordt het verzoek alsnog bediend, gewoon traag. Loopt het wachten over die timeout heen, dan retourneert nginx een 504 Gateway Timeout. Slacht FPM het verzoek zelf af omdat request_terminate_timeout is overschreden, dan retourneert nginx een 502 Bad Gateway.
Zodra de pool zijn plafond raakt, schrijft FPM ook een regel in zijn error log die er zo uitziet:
WARNING: [pool www] server reached pm.max_children setting (10), consider raising it
Die regel, terug te vinden in de FPM-broncode zelf, is hét canonieke teken van een uitgeputte pool. Zie je hem, dan is de pool te klein voor de werklast, de werklast te zwaar voor de pool, of allebei.
Waarom dit zo ontworpen is
Een vast workerplafond lijkt beperkend, totdat je bedenkt wat het alternatief oplevert. Elke PHP worker houdt geheugen vast: WordPress core, alle actieve plugins, de opcode cache-voetafdruk en de allocaties per request. Een typische WordPress-worker zit qua resident set ergens tussen de 60 MB en 200 MB, afhankelijk van de pluginstack. Zou FPM workers ongelimiteerd opschalen, dan groeit het geheugengebruik bij een verkeerspiek door tot de OOM killer van de kernel PHP-FPM (of erger nog, de database) afschiet. Een gecapte pool faalt voorspelbaar (trage verzoeken, een wachtrij, uiteindelijk 502/504) in plaats van catastrofaal (de hele server offline).
Het pool-model isoleert ook de slechte dag van de ene site van de rest van de server. Begint een plugin queries van 10 seconden te draaien, dan houden die queries workers bezet maar kunnen ze de onderliggende machine niet leegtrekken voorbij het plafond. De host kan de pool dimensioneren op basis van het beschikbare RAM en weet zo wat het worst case-scenario is.
Wat dit praktisch betekent voor een WordPress-site
Hoeveel workers een site aankan, hangt af van twee dingen: hoeveel RAM elke worker gebruikt en hoe lang elk verzoek duurt. Gebruikt een worker 120 MB en heeft een server 4 GB RAM beschikbaar voor FPM, dan is de bovengrens ruwweg 33 workers, hoeveel je er ook zou willen. Duurt elk verzoek 200 ms, dan bedienen die 33 workers ongeveer 165 verzoeken per seconde. Duurt elk verzoek 2 seconden, dan bedient diezelfde pool er 16 per seconde. De requesttijd halveren staat wiskundig gelijk aan de pool verdubbelen, alleen kost het geen extra geheugen.
Daarom domineert caching het hele gesprek. Een pagina die uit een full-page cache komt (Varnish, nginx fastcgi_cache, een LSCache- of WP Rocket-disk cache) raakt PHP-FPM helemaal niet. De workerpool is gereserveerd voor verzoeken die echt PHP nodig hebben: ingelogde gebruikers, WooCommerce-winkelwagen en checkout, REST API-aanroepen, admin-acties en alle uncacheable, gepersonaliseerde content. Een site die 95% van zijn verkeer uit cache serveert, redt zich met een kleine pool. Een site waar elk verzoek dynamisch is (een membership-site, een grote WooCommerce-shop met ingelogde klanten) heeft een grotere pool nodig of kortere verzoeken.
Daarom raakt de WordPress-admin als eerste verzadigd. Elk wp-admin-verzoek is dynamisch, draait door een zwaarder plugin-codepad dan de front-end en is niet te cachen. Een handvol admin-gebruikers kan net zoveel workers bezetten als honderden gecachete front-endbezoekers.
Zodra een pool richting verzadiging gaat, zijn de zichtbare symptomen voorspelbaar: de TTFB schiet omhoog omdat verzoeken nu wachttijd in de queue meekrijgen vóórdat de eerste byte überhaupt wordt gegenereerd, het CPU-gebruik klimt omdat elke actieve worker echt werk staat te doen, en de FPM error log laat de pm.max_children-waarschuwing zien. Bezoekers krijgen eerst trage pagina's, daarna 502- of 504-responses zodra verzoeken over de geconfigureerde timeouts heen lopen.
Wat PHP workers NIET zijn
De meeste verwarring over workers komt doordat ze worden verward met aangrenzende concepten. Ze zijn geen van deze dingen.
- Geen WordPress-gebruikers. Een ingelogde gebruiker bezit geen worker. De sessie van die gebruiker leeft in de database en in een cookie. Een worker wordt alleen even uit de pool gepakt voor de milliseconden dat één HTTP-verzoek wordt afgehandeld, en gaat dan terug. Tienduizend ingelogde gebruikers vragen niet om tienduizend workers.
- Geen bezoekers. Een worker is niet "één per bezoeker". Eén worker handelt over zijn levensduur veel opeenvolgende bezoekers af. De metric die telt is het aantal gelijktijdige verzoeken, niet het aantal gelijktijdige gebruikers.
- Geen CPU-cores. Workers en cores hangen samen, maar zijn niet hetzelfde. Een server met 4 cores kan productief méér dan 4 workers draaien zodra verzoeken I/O-bound zijn (wachten op de database, op Redis, op een externe API), omdat de cores tijdens dat wachten gewoon vrij blijven. Een veelgebruikte vuistregel is 2 tot 4 workers per core, maar het juiste aantal is wat in het beschikbare RAM past en bij het profiel van je verzoeken hoort.
- Geen databaseverbindingen. MySQL-verbindingen zijn een aparte pool met een eigen limiet (
max_connectionsinmy.cnf). Een worker houdt tijdens zijn werk normaal gesproken één DB-verbinding vast, maar de database heeft een eigen plafond en een eigen wachtrij. Workers uitputten en database-verbindingen uitputten zijn verschillende foutmodellen met verschillende oplossingen. - Geen WordPress-concept. WordPress weet niet eens dat workers bestaan. De pool is een PHP-FPM-constructie die volledig buiten WordPress wordt beheerd. Je kunt het workeraantal niet vanuit WordPress verhogen, geen plugin kan ze tonen, en
wp-config.phpheeft er geen instelling voor. De configuratie staat in/etc/php/<versie>/fpm/pool.d/*.confen vereist een herstart van de FPM-service om actief te worden.
Waar je verder kunt lezen
Wil je de pool correct configureren — de juiste procesmanager-modus kiezen, pm.max_children berekenen en de bijbehorende richtlijnen instellen — dan is de PHP-FPM tuninggids het praktische vervolg op dit artikel. Is het symptoom de oplopende serverresponstijd die een verzadigde pool veroorzaakt, lees dan het artikel over hoge TTFB; dat legt uit hoe je TTFB meet en wat realistische waardes zijn. Is het symptoom dat het dashboard hangt terwijl de front-end snel blijft, dan loopt het artikel over een trage WordPress-admin door waarom admin-verzoeken zwaarder zijn dan front-endverzoeken en de pool als eerste verzadigen. Is het symptoom dat tegelijk ook de CPU vol staat, dan dekt het artikel over hoog CPU-gebruik de samenhangende oorzaken en signalen.