WordPress
WordPress multisite audit: checklist vóór een chat-agent
Een verwarmingsgroep uit Utrecht vraagt een chat-agent op drie subsites, voor augustus. Voor we offreren, draait de auditchecklist over hun WordPress multisite.

De marketing lead van een Utrechtse verwarmingsgroep mailt op een dinsdag in mei. Ze willen een chat-agent op drie van hun vijftien subsites, voor augustus. Bijna terloops noemen ze dat het netwerk draait op WordPress 6.4 multisite, sinds 2017 live staat, en "er vast wel iets in te schroeven valt". Voor we met een prijs reageren, draait de auditchecklist. Die checklist bestaat omdat chat-agents die je op een trage multisite vastschroeft, de reden worden dat de homepage om 14:00 begint te timen.
Dit is wat we scoren, in welke volgorde, en waarom die volgorde uitmaakt.
Eerst wp_options openen
Een chat-agent is een kleine client. Hij opent een session, post naar een REST-endpoint, krijgt een antwoord, sluit. De widget zelf weegt misschien 40KB gzipped. Niets daarvan is het probleem. Het probleem is dat elke REST-request naar een WordPress-instantie de autoloaded options in het geheugen laadt voor hij de route raakt. Weegt autoload 6MB, dan heeft een chatronde van drie beurten al 18MB aan option-queries betaald voor er nuttig werk uitkomt.
De query is één regel per subsite:
SELECT SUM(LENGTH(option_value)) / 1024 / 1024 AS autoload_mb
FROM wp_2_options
WHERE autoload = 'yes';
We draaien dit over elke wp_N_options-tabel in het netwerk, en daarna halen we de grootste boosdoeners eruit:
SELECT option_name, LENGTH(option_value) AS bytes
FROM wp_2_options
WHERE autoload = 'yes'
ORDER BY bytes DESC
LIMIT 15;
De rode lijn van 4MB autoload
De grenzen die we hanteren, tegen zo'n vijftig Nederlandse mkb-multisites in de afgelopen twee jaar:
- Onder 1MB: gezond. Lever de agent op.
- 1 tot 3MB: monitoren. Doe een Query Monitor-ronde na livegang.
- 3 tot 4MB: eerst opruimen. De agent werkt, maar de pagina die hem host verliest 200 tot 400ms.
- Boven 4MB: rode lijn. Wij offreren niet tot de tabel kleiner is.
Het getal 4MB is niet willekeurig. Boven dat punt kost PHP's serialize en unserialize op elke request meer dan de eigenlijke round trip van de chat-agent naar de LLM. Je debugt een 'trage chatbot' die eigenlijk een trage site is.
Bulk-delete nooit autoloaded options zonder snapshot. Yoast Indexable, Polylang-strings, WooCommerce attribute-lookups en Elementor cache-flags staan er allemaal in, en op z'n minst één ervan is dragend voor een subsite die je vergeten was te bestaan.
De gebruikelijke daders in Nederlandse mkb-stores: WooCommerce session-rijen die nooit een transient zijn geworden, Yoast indexable build-artefacten, restjes WPML-vertaallogs van een migratie die niemand heeft afgemaakt, en de diep optimistische debug-payloads die sommige plugin-makers rechtstreeks in wp_options wegschrijven.
REST-exposure op de veertien plugins die we altijd terugzien
Elke Nederlandse mkb-multisite die we auditen heeft dezelfde plugin-shortlist. De cast over ruwweg zestig audits: WooCommerce, Yoast SEO, Elementor, Advanced Custom Fields, WP Rocket, Wordfence, UpdraftPlus, Contact Form 7, WPForms, WP Mail SMTP, Polylang, MonsterInsights, Akismet, en Rank Math (waar Yoast vandaan is gemigreerd). Elk hiervan exposeert z'n eigen REST-namespace of lift mee op /wp/v2/. Sommige routes mag een publieke chat-agent prima lezen. Sommige lekken data die de marketing lead liever niet voorgelezen krijgt.
Enumeratie is één curl per subsite:
curl -s https://shop2.example.nl/wp-json/ \
| jq '.routes | keys[]' \
| sort -u
Daarna lezen we elke namespace tegen het WordPress REST API handbook en de eigen route-map van de plugin. De routes die we in eerste ronde rood scoren:
GET /wp/v2/usersdie iets anders dan een lege array teruggeeft voor anonieme callers. WordPress veranderde het standaardgedrag in 4.7.1, maar een lange staart oudere configs lekt nog steeds author-slugs.GET /wc/store/cartblootgesteld aan de agent zonder nonce-afhandeling. De WooCommerce Store API is gebouwd voor headless checkout en geeft cart-inhoud terug aan wie het correct vraagt.GET /acf/v3/options/optionsals ACF op de defaults staat. We hebben API-keys in de response zien staan.- Contact Form 7's
/contact-form-7/v1/contact-formsdie form-IDs en veldnamen toont aan anonieme callers. Precies dat is wat in form-spam-lijsten wordt gescraped.
We blokkeren de agent niet om REST te raken. We scoren welke routes hij als ongeauthenticeerde client mag aanroepen, welke een application password nodig hebben, en welke hij nooit mag aanraken. Die tabel komt in de offerte.
De wp-cron-vraag, en de abandoned-cart-job van 02:30
WordPress' pseudo-cron is het stuk van de stack dat het hardst breekt als een chat-agent live gaat. De default wp-cron.php vuurt op page load, en een chat-agent die het verkeer met zelfs 30% verhoogt, verhoogt het afvuren van cron, en die vecht op zijn beurt met de agent om PHP-FPM workers. Het standaardadvies, volgens de WordPress developer handbook, is DISABLE_WP_CRON zetten en wp-cron.php aanslaan vanuit een echte system cron.
De vraag die we per subsite moeten beantwoorden voor we die switch omzetten: welke schedules zijn nu afhankelijk van pseudo-cron die afgaat zodra een bezoeker komt? In Nederlandse e-commerce-netwerken bevat het antwoord bijna altijd een WooCommerce abandoned-cart-job ingepland rond 02:30, want dat is wanneer de winkel dood is en de plugin-auteur aannam dat het verkeer nul was. Wordt wp-cron uitgezet en vervangt geen system cron hem, dan stopt de 02:30-job stil, en drie weken later vraagt iemand waarom de recovery-mails zijn opgedroogd.
Scheduled events lijsten met WP-CLI gaat snel:
for site in $(wp site list --field=url); do
echo "== $site =="
wp cron event list \
--url="$site" \
--fields=hook,next_run_relative,recurrence \
--format=csv \
| grep -Ei 'abandoned|cart|recovery|woocommerce'
done
Wat we terugrapporteren is het antwoord op één precieze vraag: welke subsites zouden een DISABLE_WP_CRON-flip overleven zonder dat er een echte cron staat? Op een typisch Nederlands netwerk van vijftien subsites vinden we er meestal drie. De drie die het overleven: de corporate brochure-subsite zonder WooCommerce, de Nederlandstalige press-room-subsite die alleen scheduled posts gebruikt (die op bezoek toch wel vuren), en de staging-achtige subsite die een stagiair in 2022 vergeten is te wissen. De rest heeft een echte system cron-entry nodig voor de agent live gaat.
De overdracht van één A4
De output van de audit is één A4 per subsite met drie scores: autoload-gewicht in MB, REST-exposure op een schaal van A tot E tegen de plugin-shortlist, en cron-overleving als ja of nee. Geen proza. Geen managementsamenvatting. De marketing lead stuurt het door naar hun developer, en die fixt de roden in een sprint of tekent ervoor dat de chat-agent retrofit pas later komt.
Het patroon onder dit alles: het model is zelden het moeilijke deel. Het moeilijke deel is het oppervlak waar het model mee mag praten. Een autoload-tabel van 6MB is oppervlak. Een lekkende /wp/v2/users is oppervlak. Een verweesde 02:30-cron-job is oppervlak. Je kunt het agent-budget besteden aan een slimmer model, of je kunt het besteden aan een host die ophoudt met zichzelf te vechten. Wij kiezen elke keer voor het tweede.
Het kleinste wat je vandaag kunt doen, als je een WordPress multisite draait en iemand een chat-agent-project heeft geopperd: open een terminal, draai de autoload-query hierboven tegen elke wp_N_options-tabel, en schrijf het antwoord in één kolom op papier. Staat er een getal boven de 4, dan is het antwoord op 'kunnen we er een chat-agent in zetten' 'nog niet'.
Toen we afgelopen winter de chat-agent bouwden voor een Rotterdamse groothandel met een WooCommerce-netwerk van negen subsites, was de autoload op hun vlaggenschip-subsite 7,1MB en gaf één van hun REST-routes het e-mailadres van iedere klant terug. We besteedden de eerste week van dat project niet aan de agent. Die week ging in de tabel zitten.
Kern
Ligt wp_options autoload op een willekeurige subsite boven de 4MB, dan scoor je de chat-agent retrofit als nog niet. Eerst de tabel fixen, dan over het model praten.
FAQ
Waarom scoor je wp_options autoload als eerste?
Elke REST-request laadt autoloaded options in het geheugen. Een chat-agent vuurt drie tot vijf REST-calls per beurt af, dus bloat stapelt snel op en verandert een response van 200ms in een wachttijd van een seconde waar de gebruiker het model de schuld van geeft.
Is 4MB een harde grens of een richtlijn?
Richtlijn. Onder 1MB is gezond; boven 4MB beginnen PHP's serialize en unserialize meer te kosten dan de LLM-round trip. De exacte lijn hangt van de hosting af, maar 4MB is waar wij stoppen met nieuw werk offreren.
Waarom scheduled events per subsite lijsten voor je wp-cron uitzet?
WordPress pseudo-cron vuurt op bezoekersload. Uitzetten zonder echte system cron doodt jobs als de WooCommerce abandoned-cart-veeg van 02:30 in stilte. De audit rapporteert welke subsites het zouden merken en welke niet.
Wat als de klant het opruimwerk vooraf weigert?
Dan offreren we de agent tegen een hoger tarief, schrijven we het autoload-getal in het contract, en tekenen we beiden dat we een tragere widget verwachten. In twee jaar heeft precies één klant die deal aangenomen.