123 —Operations
PHP-FPM pools afstemmen: 2GB, 4GB en 8GB VPS vergeleken
Een WooCommerce shop crasht om lunchtijd. PHP-FPM zit op 100% poolcapaciteit en je host wil je VPS opschalen. Voor je klikt: hier is de rekensom.
Een Nederlands bureau waar we mee samenwerken stuurde ons vorige week dinsdag om 13:42 een Loom. De WooCommerce shop van hun klant, draaiend op een 4GB DigitalOcean droplet, gooide elke lunchpauze 502's. In het nginx error log stond de regel die iedereen die dit leest weleens heeft gezien:
2026-06-09 12:47:14 [error] 1142#1142: *4319 connect() to unix:/run/php/php8.2-fpm.sock failed (11: Resource temporarily unavailable) while connecting to upstream
De hostingmaatschappij wilde dat ze gingen upgraden naar een 8GB box. De echte fix was één regel aanpassen in /etc/php/8.2/fpm/pool.d/www.conf en een php-fpm reload. PHP-FPM pool sizing is zo'n tuningklus waarbij de defaults zijn ingesteld op een hypothetische kleine site en daar blijven staan tot het lunchverkeer het probleem aan het licht brengt.
Deze post loopt door de pm.max_children rekensom voor de drie VPS-formaten die we het vaakst zien onder een afgemat WordPress-shop: 2GB, 4GB en 8GB. De getallen achterin zijn echt, afkomstig van productiemachines die we de afgelopen twaalf maanden in de gaten houden.
De enige formule die telt
Al het andere is een voetnoot bij dit:
pm.max_children = (Total RAM - reserved) / average PHP-FPM worker RSS
Reserved is het geheugen dat je verschuldigd bent aan de kernel, MySQL, nginx, Redis als je dat draait, en alle cron jobs die op dezelfde box lopen. Worker RSS is wat één php-fpm proces resident vasthoudt onder je echte workload, niet onder een synthetische benchmark.
Je kunt het op een live box aflezen met:
ps -ylC php-fpm8.2 --sort:rss | awk '{sum+=$8; n++} END {print "avg KB:", sum/n}'
Op een standaard WordPress 6 installatie met één page builder en twintig plugins ligt dat getal meestal tussen de 40 en 80 MB. WooCommerce met een zwaar thema en boekhoudplugins duwt het over de 100 MB. Heb je dit getal verkeerd, dan is de rest van de rekensom decoratie.
De 2GB box
Goedkoopste VPS in de catalogus, vaak één vCPU. Dit formaat zien we onder brochuresites, kleine tandartspraktijken, en het soort WordPress-shop dat 60 bestellingen per week doet. Na Ubuntu, MySQL (met innodb_buffer_pool_size op 256M), nginx, fail2ban en een kleine Redis-instantie, heb je ruwweg 900 MB over voor PHP.
Ga uit van 60 MB per worker. 900 / 60 = 15. Naar beneden afronden om ruimte te laten voor OPcache-groei en de incidentele zware request:
; /etc/php/8.2/fpm/pool.d/www.conf
pm = dynamic
pm.max_children = 14
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 6
pm.max_requests = 500
Veertien workers verzadigen een enkele vCPU voordat ze het RAM uitputten, en dat is de juiste failure mode. Zie je 502's op deze box, dan is het antwoord bijna nooit 'meer children toevoegen'. Het is caching: page cache, object cache, en een CDN voor de statische assets.
De 4GB box
De shop uit de Loom zat hier. Na MySQL met een 1 GB buffer pool, Redis, nginx en de OS is ongeveer 2,4 GB van jou. Bij 65 MB per worker komt dat neer op 36. Ze hadden pm.max_children op 5 staan, de default in sommige controlpanel-installaties. Vijf workers lopen tijdens de lunch tegen een muur bij elke shop met meer dan tien gelijktijdige checkouts.
pm = dynamic
pm.max_children = 35
pm.start_servers = 8
pm.min_spare_servers = 4
pm.max_spare_servers = 12
pm.max_requests = 500
Na de reload waren de 502's voorbij. De box zit nu rond de 60% poolgebruik tijdens de lunchpiek en 12% de rest van de dag. Geen upgrade nodig.
De 8GB box
Het formaat dat we eigenlijk aanraden voor een serieuze WooCommerce shop met 500+ bestellingen per week. Na een royale 2 GB MySQL buffer pool, 256 MB voor Redis, de OS, nginx en een back-up-agent blijft er ongeveer 5,4 GB over. Bij 80 MB per worker is dat 67. We ronden af op 70 en accepteren dat we op een vier vCPU droplet eerder CPU-bound dan memory-bound zullen zijn:
pm = dynamic
pm.max_children = 70
pm.start_servers = 12
pm.min_spare_servers = 6
pm.max_spare_servers = 18
pm.max_requests = 500
Op een box van deze grootte begint pm.max_requests ertoe te doen. Wij zetten hem op 500 om workers te recyclen die plugin memory leaks oppikken. De FPM-handleiding op php.net beschrijft dit nog steeds als de standaardverdediging tegen onbegrensde groei, en we hebben nog geen WordPress-installatie gezien waar de leak rate geen recycle om de paar honderd requests rechtvaardigt.
Drie dingen die de rekensom stilletjes om zeep helpen
De formule gaat ervan uit dat je rekening hebt gehouden met de rest die op de box draait. De drie valkuilen die we steeds vinden bij een verouderde site die op een enkele VPS draait:
De buffer pool van MySQL
De default innodb_buffer_pool_size op een verse Ubuntu-installatie is 128 MB, maar veel ops engineers zetten hem uit gewoonte op 50 tot 70% van het totale RAM. Op een 4GB box is dat 2 GB weg voor je überhaupt PHP gaat tellen. Draai mysql -e "SHOW VARIABLES LIKE 'innodb_buffer_pool_size'" op de box van de klant voor je een poolconfig opstelt.
OPcache is gedeeld
Genoeg capaciteitsrekensommen tellen opcache.memory_consumption per worker. Het is shared memory, één keer gealloceerd. Heb je hem op 256 MB gezet, dan is dat 256 MB totaal, niet 256 MB maal pm.max_children. Dit staat in de OPcache configuration docs, maar het verkeerd lezen ervan komt irritant vaak voor.
Object cache RSS leeft binnen de worker
Redis staat los. Maar de Redis client library, de WordPress object cache drop-in, en de geserialiseerde cache entries die de worker tijdens een request in zijn eigen geheugen vasthoudt, tellen allemaal mee voor de worker RSS. Op WooCommerce-shops met de officiële Redis Object Cache plugin zien we de gemiddelde worker hierdoor van 60 MB naar 90 MB gaan bij een lange productlijst-request.
De kleinste verandering die je vandaag kunt doorvoeren
Open een shell op de box. Draai de ps-oneliner hierboven om de daadwerkelijke gemiddelde RSS van je PHP-FPM workers te zien. Open /etc/php/8.2/fpm/pool.d/www.conf en check pm.max_children tegen de formule. Zit hij er ver naast (je merkt het, de afwijking is meestal 3x of meer), fix het, reload php-fpm, en kijk hoe de volgende verkeerspiek verloopt.
Toen we Pier bouwden, liepen we hier bij klant na klant tegenaan, en hoe we het uiteindelijk hebben opgelost is door de live PHP-FPM poolconfig, MySQL buffer pool size en gemiddelde worker RSS samen op één pagina te tonen die je kunt openen voor je de site aanraakt. Hij gebruikt dezelfde MySQL editor en version history als de rest van de app, dus de config-aanpassing en de reload zijn één rollback verwijderd.
Het kleinste wat je vandaag kunt doen: SSH naar de box van je drukste shop, draai de ps-oneliner, en zet de echte gemiddelde worker RSS in een comment bovenaan www.conf. De volgende keer dat iemand de pool gaat tunen, ligt de rekensom voor hem.
— Vragen —
Wat is een veilige pm.max_children voor een 4GB VPS met WooCommerce?
Rond de 30 tot 35 als de buffer pool van MySQL 1 GB is en Redis draait. Meet je daadwerkelijke worker RSS met ps voor je je vastlegt op een getal.
Moet ik pm = dynamic, static of ondemand draaien?
Dynamic voor de meeste WordPress-shops. Static als het verkeer stabiel is en je voorspelbaar geheugengebruik wilt. Ondemand alleen op dev boxes of zelden bezochte sites.
Waarom zie ik 502's terwijl de pool maar op 50% capaciteit zit?
Meestal is dat listen.backlog (default 511) of net.core.somaxconn, niet pm.max_children. Verhoog die twee voor je meer children aan de pool toevoegt.