— Artikel — № 100

100 —Operations

LiteSpeed vs Apache vs Nginx: WooCommerce-cache vergeleken

Eén WooCommerce-shop van zes jaar oud, 14k producten, één VPS. We wisselden drie keer van reverse cache en keken wat de TTFB daadwerkelijk verschoof.

Stilleven op beige linnen: gestapelde benchmarkkaarten, vhost-print, ruitjespapier, messing CACHE-plaat, rode lakzegel.
Hero · gestileerd stilleven№ 100

De shop is van een Nederlands bureau waar we mee samenwerken: een zes jaar oude WooCommerce-shop op een Hetzner-bak met 4 vCPU, 14.000 producten, een admin-paneel dat negen seconden nodig heeft om /wp-admin/edit.php?post_type=product te openen, en aanstaande vrijdagavond een uitverkoop. De eigenaar wil niet migreren naar Shopify. Hij wil dat de cart minder aanvoelt als nat karton.

We hadden een vrije middag en root op de bak, dus deden we het voor de hand liggende experiment: dezelfde site, dezelfde database, dezelfde plugins, drie reverse caches. LiteSpeed Enterprise met LSCache, Apache 2.4 met mod_cache_disk voor PHP-FPM, en Nginx met de FastCGI cache. Alle drie kregen ze dezelfde warm-up: wget --mirror over de top 200 product-URL's uit de laatste 30 dagen GA, daarna tien minuten wrk op concurrency 50.

Dit is wat we zagen, wat ons verbaasde, en de configs die de cijfers opleverden. Als jij een legacy site draait zoals deze en je moet in 2026 beslissen wat je voor PHP-FPM zet, dan is het antwoord minder voor de hand liggend dan de LiteSpeed-marketing suggereert, maar ook minder vanzelfsprekend dan de "gewoon Nginx"-consensus op Reddit.

De shop en de baseline

WooCommerce 8.4, WordPress 6.5, PHP 8.2-fpm, MariaDB 10.11, Redis voor object cache, geen CDN ervoor (bewust, omdat het bureau de origin eerlijk wilde meten). Het thema is een zwaar aangepaste Flatsome. Er zijn 41 actieve plugins. Yoast, WPML, een custom verzendcalculator, een B2B-prijzenplugin, en de gebruikelijke verzameling "dit was urgent in 2021"-resten die nooit zijn opgeruimd.

Koude TTFB op de homepage zonder reverse cache, alleen PHP-FPM achter Apache: 1.840 ms. Koude TTFB op een productpagina: 2.210 ms. Onder wrk -t4 -c50 -d60s bleef de bak hangen op 7,2 req/s en klom de load average naar 18 voordat PHP-FPM 502'jes ging gooien. Dit is de baseline. Alles hieronder wordt daartegen afgemeten.

LiteSpeed Enterprise + LSCache

LiteSpeed Enterprise kost ongeveer $26 per maand voor een 4-CPU VPS-licentie. Het bureau betaalde daar al voor. LSCache is een plugin die in de wp-content-map leeft en met de LiteSpeed-server praat via een privé cache-API, waardoor purges plaatsvinden bij post save, bij order completion en bij cart change. Dat laatste is belangrijk voor WooCommerce: een naïeve page cache serveert een verouderde mini-cart, waardoor klanten gaan refreshen en refreshen en uiteindelijk afhaken.

Het relevante blok in .htaccess nadat je LSCache hebt aangezet:

<IfModule LiteSpeed>
  CacheLookup on
  RewriteEngine On
  RewriteRule .* - [E=Cache-Control:no-cache,L] [E=cache-control:no-cache]
  RewriteCond %{REQUEST_URI} !^/(wp-admin|wp-login|cart|checkout|my-account)
  RewriteCond %{HTTP_COOKIE} !(wp-postpass|wordpress_logged_in|woocommerce_items_in_cart) [NC]
  RewriteRule .* - [E=Cache-Control:max-age=300]
</IfModule>

Warme TTFB op de homepage: 71 ms. Warme TTFB op een productpagina: 88 ms. Onder dezelfde wrk-run: 4.910 req/s, load average 1,1. De cart bleef correct updaten omdat LSCache vary-keyed op de cart-hash cookie, precies het deel dat je bij nginx_fastcgi_cache zelf met de hand moet bouwen.

Het nadeel is wat je zou verwachten: het is een gesloten, betaald product, de cache-map heeft eigen rechten-eigenaardigheden (hij staat onder /usr/local/lsws/cachedata/, niet in je site-root, wat het debuggen van permissies de eerste keer verwarrend maakt), en zodra je naar een host gaat die geen LiteSpeed-licentie meelevert, ben je alles opnieuw aan het schrijven.

Apache 2.4 + mod_cache_disk

Het native Apache-verhaal is mod_cache_disk voor mod_proxy_fcgi. Het is de optie die je pakt als de hosting je Apache geeft en je geen tweede daemon aan het schema wilt toevoegen.

CacheQuickHandler off
CacheLock on
CacheLockPath /tmp/mod_cache-lock
CacheLockMaxAge 5
CacheRoot /var/cache/apache2/woo
CacheEnable disk /
CacheDirLevels 2
CacheDirLength 1
CacheIgnoreHeaders Set-Cookie
CacheIgnoreNoLastMod On
CacheDefaultExpire 300
CacheMaxExpire 600

<LocationMatch "^/(cart|checkout|my-account|wp-admin|wp-login)">
  CacheDisable on
</LocationMatch>

Warme TTFB op de homepage: 148 ms. Productpagina: 171 ms. Onder wrk: 2.140 req/s, load average 3,4. De Apache-documentatie voor mod_cache is écht goed en het is de moeite waard om die één keer door te lezen voordat je iets gaat tunen.

Het eerlijke probleem met deze configuratie is invalidatie. mod_cache_disk weet niet dat je net een post hebt gepubliceerd. We knoopten een klein WordPress mu-plugin in elkaar dat htcacheclean -p /var/cache/apache2/woo -t draait op save_post en woocommerce_update_product. Dat werkt, maar het is het soort lijmwerk waar je niet meer aan denkt totdat een klant belt over een prijs die niet bijgewerkt is. Er bestaat een CacheSocache-backend die shared memory gebruikt en sneller is bij hits, maar die overleeft een restart niet, en dat wilden we niet voor een uitverkoop op vrijdag.

De Set-Cookie-val

WooCommerce zet bij bijna elke ongecachede pageview een cookie (woocommerce_cart_hash, de session cookie, de recently-viewed-products cookie). Apache weigert standaard om een response te cachen die een Set-Cookie-header bevat, waardoor je cache hit rate op nul blijft hangen en je gaat aannemen dat de cache stuk is. CacheIgnoreHeaders Set-Cookie is de regel die dat oplost, in combinatie met een zorgvuldige LocationMatch voor de pagina's die echt per-user state nodig hebben.

Nginx + FastCGI cache

De Nginx-config die onze cijfers opleverde, ingekort tot de cache-relevante regels:

fastcgi_cache_path /var/cache/nginx/woo levels=1:2 keys_zone=WOO:100m
                   inactive=60m max_size=2g use_temp_path=off;
fastcgi_cache_key "$scheme$request_method$host$request_uri";

map $http_cookie $skip_cache {
  default 0;
  "~*wordpress_logged_in"        1;
  "~*woocommerce_items_in_cart=1" 1;
  "~*wp_woocommerce_session"     1;
}

map $request_uri $skip_cache_uri {
  default 0;
  "~*/(cart|checkout|my-account|wp-admin|wp-login)" 1;
}

location ~ \.php$ {
  fastcgi_pass unix:/run/php/php8.2-fpm.sock;
  fastcgi_cache WOO;
  fastcgi_cache_valid 200 301 302 5m;
  fastcgi_cache_bypass $skip_cache $skip_cache_uri;
  fastcgi_no_cache    $skip_cache $skip_cache_uri;
  add_header X-Cache $upstream_cache_status;
}

Warme TTFB op de homepage: 62 ms. Productpagina: 74 ms. Onder wrk: 6.380 req/s, load average 0,9. Dit is het snelste resultaat van de drie en op papier zou dit het antwoord moeten zijn.

Wat de cijfers niet laten zien, is dat we hier twee uur over deden. De cookie-map hierboven is de derde versie. De eerste negeerde de cart-items cookie en serveerde ingelogde gebruikers elkaars mini-carts (we hebben het op staging opgevangen, op het nippertje). De tweede cachede de checkout-endpoint omdat we de regex niet hadden vastgezet. De derde is degene die we nu in elke Nginx + WooCommerce-setup overnemen, en we lezen hem nog steeds opnieuw door voordat we hem in productie zetten.

De drie getallen, naast elkaar

Koude TTFB homepage / warme TTFB homepage / sustained req/s onder wrk -t4 -c50 -d60s:

  • LiteSpeed + LSCache: 1.840 ms / 71 ms / 4.910 req/s
  • Apache + mod_cache_disk: 1.840 ms / 148 ms / 2.140 req/s
  • Nginx + FastCGI cache: 1.840 ms / 62 ms / 6.380 req/s

Nginx wint op pure doorvoer. LiteSpeed wint op time-to-correctness, omdat de WooCommerce-bewuste vary-logica met de plugin meekomt in plaats van een regex te zijn die jij om 22:00 hebt geschreven. Apache verliest op beide vlakken, maar het is de enige van de drie die geen nieuwe daemon of betaalde licentie nodig heeft, en voor een site die 30k pageviews per dag doet, is een TTFB van 148 ms echt prima.

Wanneer kies je wat

Heb je al LiteSpeed omdat je hostingmaatschappij het meelevert (veel cPanel-hosts doen dat tegenwoordig), hou het dan. De LSCache-plugin heeft het minst slechte cache-invalidatieverhaal in de WordPress-wereld, en je middag is beter besteed aan andere problemen. De LSCache-docs zijn ongebruikelijk eerlijk over de trade-offs.

Draai je op een VPS waar je zelf de baas bent en heb je een ops-persoon die regex zonder fronsen leest, dan geeft Nginx + FastCGI cache je de beste doorvoer en de meeste knoppen om aan te draaien. Reserveer die twee uur. Lees de cookie-map twee keer voordat je hem pusht. Voeg add_header X-Cache $upstream_cache_status toe en laat het in productie aanstaan: je zult jezelf dankbaar zijn de eerste keer dat een klant een verouderde prijs meldt.

Zit je vast aan Apache omdat de host of de bestaande .htaccess-knoop een swap riskant maakt, dan is mod_cache_disk plus een kleine mu-plugin voor invalidatie echt werkbaar. Het is trager dan de andere twee, maar het gat met de baseline is enorm en het gat met Nginx is voor de meeste shops onzichtbaar voor de klant.

Het deel dat we niet gemeten hebben

Geen van deze cijfers omvat database-queries. De reverse cache verbergt de database zolang de pagina warm is. Op het moment dat een ingelogde klant op checkout klikt, of de admin edit.php?post_type=product opent, stapt de cache opzij en zit je terug bij Woo's queryplan. Daar zitten de meeste "mijn shop is traag"-tickets daadwerkelijk, en geen enkele hoeveelheid reverse-cache-tuning lost dat op.

Toen we Pier bouwden, liepen we hier herhaaldelijk tegenaan: de cache is snel, de front-end ziet er goed uit, en dan opent iemand de admin en valt de site om omdat wp_postmeta geen index heeft op de kolom waar WooCommerce constant op filtert. Hoe we het uiteindelijk hebben opgelost, was door de operator een chat-gekoppelde MySQL editor te geven naast de bestandenboom, plus version history voor elke schema-wijziging, zodat de "voeg een index toe, kijk of het helpt, draai terug als het niets oplevert"-loop dertig seconden duurt in plaats van een onderhoudsvenster.

Het kleinste wat je vandaag kunt doen: SSH naar je bak, draai curl -w "%{time_starttransfer}\n" -o /dev/null -s https://jouwsite/ tien keer tegen je homepage, schrijf de mediaan op. Doe dan hetzelfde voor een productpagina. Die twee getallen vertellen je of het volgende wat je moet aanpakken de reverse cache is, de database, of geen van beide.

— Vragen —

Werkt LSCache ook buiten LiteSpeed-servers?

Nee. De plugin praat met een privé cache-API in de LiteSpeed-server. Installeer hem op Apache of Nginx en de page-cache-features doen stilzwijgend niets, hoewel de object cache wel blijft werken.

Waarom niet gewoon Cloudflare ervoor en de origin cache overslaan?

Cloudflare helpt anonieme traffic, maar cachet geen ingelogde WooCommerce-sessions of admin-pagina's. Je hebt nog steeds een origin cache nodig, want het trage pad is precies waar je klanten over klagen.

Is mod_cache_disk veilig onder hoge schrijfbelasting?

Ja, mits CacheLock aanstaat en CacheLockPath naar een snel filesystem wijst. Onder zware purge-belasting kunnen de lock-bestanden zich opstapelen, dus draai htcacheclean op een cron of via een save_post-hook.