WordPress wp-cron: waarom het misgaat en hoe je het vervangt

WP-Cron lijkt op een cron-daemon maar is het niet. Het is een PHP-routine die alleen draait zodra een bezoeker je site raakt, en precies dat is de reden dat ingeplande posts verdwijnen, backups hun tijdslot missen en WooCommerce-mails uren te laat aankomen. Dit artikel legt uit wat WP-Cron echt is, waarom het faalt en hoe je het vervangt door een echte system cron die gewoon draait wanneer hij moet.

Als ingeplande posts niet verschijnen, backups hun tijdslot missen of WooCommerce-bestellingsmails uren te laat binnenkomen, dan draait het bijna altijd om hetzelfde: WP-Cron doet niet wat mensen denken dat hij doet. WP-Cron is geen achtergrondproces. Het is een PHP-routine die pas afgaat als er een bezoeker op de site landt, en er zijn vijf goed gedocumenteerde faalmodi die samen bijna elke "wp-cron werkt niet" melding verklaren.

Wat WP-Cron eigenlijk is

WP-Cron is een pseudo-scheduler die ingebakken zit in WordPress core. Met een Unix cron-daemon heeft hij behalve de naam eigenlijk niets te maken.

Elke keer dat er een pagina geladen wordt op een WordPress-site (niet elke admin-ajax call, maar wel iedere normale front-end of admin-request) start WordPress op en roept spawn_cron() aan. Die functie leest de cron optie uit wp_options, kijkt welke events aan de beurt zijn, checkt een transient genaamd doing_cron om te zien of er niet net al een cron-run gestart is binnen de laatste WP_CRON_LOCK_TIMEOUT seconden (standaard 60), en als er events openstaan en de lock vrij is, vuurt hij een non-blocking HTTP POST af naar wp-cron.php op dezelfde host. Die HTTP-request neemt een query parameter doing_wp_cron mee met een microtime-waarde van 22 cijfers die als lock key dient. Het proces in wp-cron.php draait vervolgens de actions die aan de beurt zijn en sluit af. Dit hele mechanisme staat beschreven in de Plugin Handbook.

Daaruit volgen direct twee dingen.

Ten eerste: geen page loads betekent geen cron-run. Als je site gepland staat om een post te publiceren om 09:00 en de eerste bezoeker komt pas om 11:30 binnen, dan verschijnt de post om 11:30. De Plugin Handbook zegt dat letterlijk: planningsfouten kunnen optreden "als je een taak inplant voor 2:00PM en er geen page loads zijn tot 5:00PM". Dit is een ontwerpkeuze, geen bug.

Ten tweede: de loopback HTTP-request is een kritiek pad. Als iets de server verhindert om een HTTP-verzoek naar zichzelf te doen, dan draait die cron-run gewoon niet. De page load van de bezoeker loopt verder normaal af. Er komt geen zichtbare foutmelding. Ingeplande events stapelen zich alleen stilletjes op.

WordPress komt standaard met vier schedules: hourly, twicedaily, daily en (sinds WordPress 5.4) weekly. Plugins kunnen eigen intervallen registreren via de cron_schedules filter hook.

Wat er allemaal afhangt van WP-Cron

Meer dan je denkt. Al deze dingen stoppen met werken zodra WP-Cron kapot is:

  • Het publiceren van ingeplande posts
  • De checks voor plugin- en theme-updates
  • Het opruimen van verlopen transients
  • Elke aanroep van wp_schedule_event() vanuit plugins: backup plugins, nieuwsbriefverzenders, SEO sitemap-rebuilds, security scanners, cache warmers
  • WooCommerce-bestellingsmails en stock-synchronisatie, omdat Action Scheduler bovenop WP-Cron zit

Even specifiek over WooCommerce. Action Scheduler is vanaf WooCommerce 4.0 meegebundeld in core, en op elke WooCommerce-site sindsdien draaien er dus twee cron-systemen boven elkaar: de native WP-Cron van WordPress core plus de eigen queue runner van Action Scheduler. Action Scheduler is niet keihard afhankelijk van WP-Cron (hij kan ook direct via de action_scheduler_run_queue hook getriggerd worden), maar hij haakt op niet-admin-verkeer standaard in op WP-Cron. Valt WP-Cron weg, dan vallen je bestelmails, abonnementsverlengingen en stock-sync jobs er ook bij neer. Op hol geslagen cron-jobs kunnen bovendien 429 Too Many Requests triggeren vanwege externe API rate limits.

De vijf faalmodi waar bijna alles op neerkomt

Uit heel veel "wp-cron werkt niet" rapporten komt bijna altijd één van deze vijf naar boven.

1. Geen verkeer of zware full-page caching

Geen page load betekent geen spawn_cron(). Dit raakt staging-sites, weinig bezochte blogs en sites achter agressieve full-page caching (een Varnish-laag, nginx fastcgi_cache, LSCache of WP Rocket disk cache, Cloudflare Cache Reserve). Een verzoek dat vanuit een reverse-proxy cache beantwoord wordt, bereikt PHP niet eens, WordPress start niet op, en er komt dus geen cron-spawn. Een volledig gecachete site kan dagenlang geen cron-run hebben tot een ingelogde admin door de cache heen breekt.

2. De loopback-request wordt geblokkeerd

Dit is technisch de subtielste faalmodus en waarschijnlijk de meest voorkomende op managed hosting. De non-blocking HTTP POST die spawn_cron() naar wp-cron.php vuurt, faalt stil. Alles wat op het netwerkpad tussen het PHP-proces en de webserver op dezelfde host zit, kan dat veroorzaken. Het Advanced Administration Handbook over loopbacks noemt de gebruikelijke verdachten, en WP Crontrol heeft een cURL-error tabel voor de gangbare faalcodes. De concrete oorzaken die ik het vaakst tegenkom:

  • BasicAuth over de hele site, inclusief wp-cron.php, geeft HTTP 401 op de loopback
  • Een firewall of security plugin blokkeert "server naar zichzelf" verzoeken standaard
  • Cloudflare of een CDN bot-rule blokkeert verzoeken die van het origin IP komen
  • Een SSL-misconfiguratie geeft cURL error 35 (TLS handshake failure) op de loopback
  • DNS-resolutie op het origin faalt, cURL error 6
  • wp-cron.php is geblokkeerd op webserver-niveau via .htaccess of nginx-regels (HTTP 403)
  • wp-cron.php is verwijderd of hernoemd als "cron disabled" zonder dat DISABLE_WP_CRON gezet is, HTTP 404
  • Een fatale PHP-fout in een plugin of thema laat de wp-cron.php request crashen met HTTP 500

3. DISABLE_WP_CRON staat aan zonder vervanger

define( 'DISABLE_WP_CRON', true ); in wp-config.php zorgt ervoor dat spawn_cron() nooit meer vuurt. De constante zit al heel lang als configuratieoptie in core. Hij verwijdert geen enkel event: de cron optie in wp_options blijft gewoon staan, wp-cron.php werkt nog steeds als je hem direct aanroept, plugins kunnen nog steeds nieuwe events registreren. De constante haalt alleen de trigger op page load weg.

Dit is de enige meest voorkomende oorzaak op managed hosting. Veel managed WordPress hosts (WP Engine, Cloudways, Kinsta, SpinupWP) zetten DISABLE_WP_CRON standaard in hun wp-config.php template en vervangen hem door hun eigen runner. Als die runner kapot gaat of de site wordt naar een andere host gemigreerd en de runner blijft achter, dan triggert niets WP-Cron meer en stoppen alle ingeplande jobs in stilte. De site oogt verder prima. Ingeplande posts komen alleen nooit online.

4. Lang lopende events en lock contention

WP_CRON_LOCK_TIMEOUT staat standaard op 60 seconden. Als één cron-event (een grote backup, een megabyte-zware feed-import, een site-brede regeneratie) langer draait dan dat, dan verloopt de transient lock. Latere page loads kunnen dan nieuwe cron-processen starten terwijl de originele nog draait, en die twee runs kunnen met elkaar vechten over hetzelfde event. Andersom: een traag event kan de lock zo lang vasthouden dat andere events hun venster missen. De documentatie van WP Crontrol over gemiste events beschrijft dit patroon.

5. Race conditions op drukke sites

Meerdere gelijktijdige page loads kunnen allemaal spawn_cron() aanroepen voordat de eerste de transient lock gezet heeft. Ze starten dan allemaal een apart wp-cron.php proces. Al die processen proberen vervolgens dezelfde due events te draaien. Het lock-mechanisme is niet volledig atomair, zeker niet over een multi-server setup waar meerdere PHP-FPM hosts dezelfde database delen maar geen lokale state. Dit is een erkende beperking, bijgehouden in WordPress Trac ticket #57924.

Een kapotte WP-Cron diagnosticeren

Er zijn drie niet-destructieve manieren om te zien of WP-Cron draait, in oplopende volgorde van betrouwbaarheid.

Site Health. Ga in de WordPress-admin naar Gereedschap, Site-status, tabblad Info, en dan "Geplande evenementen". Dit doet een loopback-test en meldt of spawn_cron() wp-cron.php kan bereiken. Zo niet, dan rapporteert Site Health de cURL-error direct.

WP Crontrol. De WP Crontrol plugin toont elk geregistreerd event, wanneer het weer moet draaien en markeert events waarvan de geplande tijd al voorbij is. Zie je events in het verleden staan, dan draait de runner niet.

WP-CLI, de harde check. Over SSH, op elke host met WP-CLI geïnstalleerd:

# Draai dit vanuit de site-root
wp cron test                       # test of het spawn-mechanisme werkt
wp cron event list --fields=hook,next_run,recurrence  # alle events plus wanneer ze weer draaien
wp cron event list --due-now       # alleen overdue events

wp cron test geeft je een schone pass of fail plus de exacte foutmelding als het faalt. Het is de snelste manier om een definitief antwoord te krijgen. De volledige command-reference staat in de WP-CLI cron command documentatie.

Drie dingen worden vaak verward met een kapotte WP-Cron, en die zijn het waard om eerst uit te sluiten:

  • Een "Missed Schedule" status op een post betekent niet dat de server plat lag. Het betekent dat de cron-runner niet op tijd vuurde. De site kan prima honderden requests per minuut serveren terwijl events zich ondertussen ophopen.
  • Een trage scheduled job is niet hetzelfde als een ontbrekende. Als een job wel draait maar vijf minuten duurt, dan zit je met een ander probleem (zie verderop bij performance).
  • Een plugin met een eigen scheduler (zoals Action Scheduler op WooCommerce, of de async task runner in sommige membership plugins) kan vastlopen terwijl WP-Cron prima werkt. Check altijd eerst de eigen queue van de plugin.

Wat het kost op drukke sites

Op drukke sites heeft het standaardgedrag van WP-Cron meetbare performance-impact. De non-blocking loopback POST zou geen latency mogen toevoegen aan de page load van de bezoeker, en de originele comment in de header van wp-cron.php claimt dat ook letterlijk. WordPress Trac ticket #18738 erkent dat die claim in de praktijk niet klopt. In omgevingen waarin het opzetten van de loopback tijd kost (TLS handshake, DNS lookup, firewall inspectie, een trage reverse proxy), betaalt de bezoeker wiens request de cron-spawn triggerde die tijd als extra TTFB.

PHP-FPM dempt dit met fastcgi_finish_request(), wat de response naar de client flusht voordat het PHP-proces klaar is. Op FPM-sites houdt dat de zichtbare latency laag. Op niet-FPM setups, of waar die optimalisatie om een of andere reden niet werkt, kan de cron-spawn terugkomen in de TTFB-cijfers van de site als een terugkerende piek.

En er is een tweede kostenpost. Elke request die cron triggert, vecht om dezelfde PHP worker pool die ook het normale verkeer afhandelt. Op een kleine pool kan een lang lopend cron-event zomaar een worker vasthouden en ander verkeer de rij in duwen. Dit is vaak wat mensen bedoelen als ze melden dat de site om de zoveel uur zomaar traag aanvoelt zonder duidelijke reden of dat de CPU-baseline structureel niet bij het verkeer past.

WP-Cron van het request-pad af halen lost beide problemen in één klap op. Meteen ook de betrouwbaarheidsproblemen uit de vijf faalmodi hierboven, want een system cron is per ontwerp immuun voor verkeersvolume, loopback-blokkades, lock contention en race conditions.

WP-Cron vervangen door een echte system cron

De aanbevolen aanpak bestaat uit twee stappen. Eerst haal je de trigger op page load weg, daarna voeg je een externe runner toe die de WordPress cron-code op een vast interval aanroept.

Stap 1: de trigger op page load uitzetten

Open wp-config.php via de bestandsbeheerder van je hostingpanel (of download het bestand via SFTP) en voeg deze regel toe, ergens boven de regel "/* That's all, stop editing! */":

// Laat wp-cron niet meer afgaan op elke page load.
// Dit werkt alleen als je hieronder een externe runner configureert.
define( 'DISABLE_WP_CRON', true );

Deze regel is niet destructief. Hij zorgt er alleen voor dat spawn_cron() niet meer draait op elke front-end request. Ingeplande events blijven gewoon in de database staan. Tot je een externe runner toevoegt, loopt er alleen niets.

Stap 2: een cronjob toevoegen via je hostingpanel

Open in cPanel (of het panel van je host) "Cron Jobs", kies "Every minute" (of "Common Settings, Once per five minutes" als je dat prettiger vindt) en zet als commando:

wget -q -O /dev/null "https://jouwsite.nl/wp-cron.php?doing_wp_cron" > /dev/null 2>&1

De meeste shared hosts bieden geen WP-CLI via cron, dus is wget de realistische keuze. Als de loopback van die host in orde is (geen BasicAuth, geen bot-rule die het origin IP blokkeert) werkt dit betrouwbaar. Heeft de host de loopback strak dichtgezet, neem dan contact op met support en vraag ze wp-cron.php te whitelisten voor origin-verzoeken.

Deze aanpak staat gedocumenteerd in de Plugin Handbook gids voor WP-Cron in de system task scheduler.

  • Waarom elke minuut? De ingebouwde intervallen van WordPress beginnen bij hourly, maar plugins kunnen zelf subuur-intervallen registreren (nieuwsbrieven om de vijf minuten, feed-pulls om de tien). Elke minuut draaien garandeert dat het kortste geregistreerde interval gehaald wordt. Weet je zeker dat je site alleen hourly of langer heeft, dan is elke vijf minuten ook prima.

Heb je SSH-toegang: een Linux crontab-regel met WP-CLI

Op een Linux-host met WP-CLI geïnstalleerd kun je de HTTP-omweg helemaal overslaan. Voeg één regel toe aan de crontab van de webserver-user (meestal www-data op Debian en Ubuntu, nginx of apache op Red Hat, of een dedicated site-user op managed hosts). Open de crontab met crontab -e als die user en voeg toe:

* * * * * /usr/local/bin/wp cron event run --due-now --path=/var/www/jouwsite.nl/htdocs --quiet > /dev/null 2>&1

Veld voor veld is dit het standaard Linux crontab(5) vijf-velden formaat: minuut, uur, dag van de maand, maand, dag van de week. * * * * * betekent "elke minuut, elk uur, elke dag, elke maand, elke dag van de week", en dat is elke minuut.

  • Waarom WP-CLI in plaats van wget? WP-CLI omzeilt HTTP volledig. Hij start de WordPress bootstrap rechtstreeks in de PHP CLI context, en dat haalt de hele loopback-faalmodus in één keer weg. Geen BasicAuth, geen firewall, geen TLS handshake. Op managed hosts waar de loopback het probleem was, is dit alleen al de fix.
  • Waarom --quiet? Cron mailt de output van elke run naar de system user. Zonder --quiet krijg je elke minuut een mail. Mét --quiet zie je alleen errors nog.
  • Waarom > /dev/null 2>&1? Het omleiden van stdout en stderr naar /dev/null is een dubbele onderdrukking bovenop --quiet, voor het geval de cron-mailer op het systeem verkeerd staat ingesteld en anders een volle mailbox oplevert.

Controleren of de vervanging werkt

Ga er niet vanuit dat het werkt omdat de cronjob geaccepteerd werd. Controleer het.

Check met een ingeplande post. Plan een testpost in voor twee minuten later via wp-admin. Wacht. Refresh de voorkant. Staat de post er op de verwachte tijd, dan vuurt de system cron volgens schema. Dit is de simpelste en meest overtuigende check.

Check Action Scheduler als WooCommerce draait. Ga in wp-admin naar Gereedschap, dan "Scheduled Actions". Sorteer op "Scheduled" en kijk of er rijen met een datum in het verleden staan. Schuiven er nieuwe rijen naar "Completed" sinds je cronjob live is, dan loopt Action Scheduler bij via precies dat WP-Cron pad dat je net hersteld hebt.

Kijk naar de TTFB op front-end requests. Verdwijnt de terugkerende TTFB-piek die je elke paar minuten zag nu WP-Cron van het request-pad af is, dan werkt de performance-kant van de fix ook.

Heb je SSH-toegang: draai wp cron event list --due-now vanuit de site-root, wacht twee minuten, en draai het opnieuw. De overdue events moeten weg zijn (of de lijst moet korter geworden zijn). Is er niets veranderd, dan draait je cronjob niet wat jij denkt dat hij draait. Je kunt ook de cron-mail of het log checken: cron levert op Linux zijn output af in de lokale user mailbox (lezen met mail of less /var/mail/www-data). Is de mailbox leeg en staat --quiet in het commando, dan is dat een goed teken.

Wat dit niet oplost

WP-Cron naar een system cron verplaatsen lost niet op:

  • Plugins die failed events in een krappe loop blijven inplannen (die events blijven stapelen, ze stapelen alleen onder een betrouwbare runner in plaats van onder een kapotte)
  • Trage cron events die minuten duren om te draaien (daar moet je het event zelf optimaliseren of offloaden, niet de runner)
  • Action Scheduler jobs die falen door niet-gerelateerde errors (database deadlocks, third-party API timeouts, PHP memory exhausted binnen de job zelf)
  • Cache warmer plugins die afhankelijk zijn van verkeer om URLs te raken (die hebben nog steeds verkeer nodig, cron triggert alleen hun huishoudelijke taken)

Heb je WP-Cron uitgezet en met een volledige system cron ervoor in de plaats verschijnen geplande posts nog steeds niet, dan is het probleem niet meer WP-Cron. Kijk dan in het PHP error log naar fatals tijdens de cron-run, bekijk het artikel over "Briefly unavailable for scheduled maintenance" als updates binnen cron crashen, en check of er geen plugin is die de cron optie in wp_options tussendoor wegveegt of herschrijft.

De misverstanden die het hardnekkigst zijn

Drie dingen komen in elke "wp-cron werkt niet" discussie voorbij, en alle drie kloppen ze niet:

  • "WP-Cron vuurt op exacte tijden." Dat doet hij niet. Hij vuurt op page loads, met "next opportunity" semantiek. Zelfs met een system cron die elke minuut draait, loopt een job die gepland staat op 09:00:00 pas op de eerste minuut-markering na 09:00, en dat kan 09:00:47 zijn.
  • "WP-Cron uitzetten breekt ingeplande posts permanent." Niet waar. De ingeplande events staan in de database en blijven daar staan. DISABLE_WP_CRON haalt alleen de trigger weg. Een system cron pikt ze direct op zodra die in de lucht is.
  • "Een missed schedule betekent dat de server eruit lag." Ook niet. De server kan duizenden requests per minuut aan het serveren zijn terwijl de cron runner stilletjes niet afgaat. "Missed schedule" is een runner-probleem, geen server-probleem.

Samenvatting

WP-Cron is een PHP-routine die op verkeer draait, geen daemon, en dat ene feit verklaart alle betrouwbaarheidsproblemen die hij heeft. De vijf faalmodi (geen verkeer, geblokkeerde loopback, DISABLE_WP_CRON zonder runner, langlopende events, race conditions) dekken bijna elke melding. De betrouwbare fix is de trigger op page load uitzetten met DISABLE_WP_CRON en een system cron toevoegen die elke minuut wp cron event run --due-now draait. Op cPanel-hosts zonder WP-CLI is een wget-aanroep naar wp-cron.php via een cPanel cronjob de fallback. Verifieer met wp cron event list --due-now, een test-ingeplande post en, op WooCommerce, het Action Scheduler scherm.

WordPress onderhoud zonder omkijken?

Ik regel updates, backups en beveiliging, en houd performance strak—zodat storingen en traagheid niet terugkomen.

Bekijk WordPress onderhoud

Doorzoek deze site

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