Wat PHP workers zijn en waarom ze in WordPress uitgeput raken

PHP workers zijn de serverprocessen die een WordPress-verzoek omzetten in HTML. Zodra ze allemaal tegelijk bezig zijn, schuift het volgende verzoek de wachtrij in en hapert je site. Dit artikel legt het mechanisme uit, de pool-modi en de symptomen die een verzadigde pool oplevert, zodat de rest van de prestatiekennisbank pas echt klikt.

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 precies pm.max_children workers, 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 met pm.start_servers workers en schaalt tussen pm.min_spare_servers en pm.max_spare_servers idle workers, met pm.max_children als 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 na pm.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_connections in my.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.php heeft er geen instelling voor. De configuratie staat in /etc/php/<versie>/fpm/pool.d/*.conf en 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.

Klaar met terugkerende traagheid?

Traagheid komt vaak terug na snelle fixes. Professioneel onderhoud houdt updates, caching en limieten consequent op orde.

Bekijk WordPress onderhoud

Doorzoek deze site

Begin met typen om te zoeken, of blader door de kennisbank en blog.