Publication différée d'un article, envoi d'une lettre d'information, reconstruction d'un sitemap, purge de données expirées : une bonne partie du travail d'un site WordPress est confiée à des tâches planifiées. Elles reposent toutes sur WP-Cron, un mécanisme dont le nom trompe sur la nature. Ce guide explique ses limites et la façon dont nous l'avons remplacé.

WP-Cron n'est pas un cron

Un vrai planificateur, comme cron sous Linux, se réveille à heure fixe et lance ce qui doit l'être. WP-Cron fonctionne à l'inverse : à chaque chargement de page, WordPress regarde si des tâches sont arrivées à échéance et, le cas échéant, déclenche en arrière-plan une requête vers wp-cron.php qui les exécute. Sans visite, rien ne se passe.

Ce choix est compréhensible : WordPress doit fonctionner sur des hébergements mutualisés où l'on n'a pas accès au planificateur du système. Mais il produit deux défauts opposés.

Trop peu d'exécutions

Sur un site peu visité, une tâche prévue à 3 h du matin s'exécutera au premier visiteur du matin. Plus grave : derrière un cache de pages, les visites n'atteignent plus PHP du tout. Nginx sert la copie en cache, WordPress ne se réveille pas, WP-Cron ne vérifie rien. Un site bien mis en cache est donc paradoxalement celui où les tâches planifiées sont les moins fiables. Nos deux sites WordPress sont derrière un cache de proxy (voir notre guide sur ce cache) : le problème nous concernait directement.

Trop de vérifications

À l'inverse, sur un site très sollicité sans cache, chaque chargement de page déclenche la vérification. WordPress pose un verrou pour éviter les exécutions simultanées, mais le coût de la vérification et des requêtes vers wp-cron.php reste réparti sur le trafic, au moment précis où le serveur est le plus chargé.

Étape 1 : couper le déclenchement par les visites

Une constante dans wp-config.php suffit :

define( 'DISABLE_WP_CRON', true );

Elle ne désactive pas les tâches planifiées : elles restent enregistrées et continuent d'être programmées par les extensions. Elle supprime seulement leur déclenchement automatique lors des visites. Sans l'étape 2, plus aucune tâche ne s'exécute : les articles programmés ne se publient plus, et rien ne le signale. C'est pourquoi les deux étapes doivent être déployées ensemble.

Étape 2 : déclencher soi-même, à intervalle fixe

Sur un serveur classique

Une ligne de crontab pour l'utilisateur qui exécute PHP suffit :

*/5 * * * * cd /var/www/html && wp cron event run --due-now --quiet

--due-now exécute uniquement les tâches arrivées à échéance, exactement ce que ferait WP-Cron lors d'une visite. L'exécuter sous l'utilisateur du serveur web (souvent www-data) évite de créer des fichiers de cache ou d'upload appartenant à root, que WordPress ne pourrait plus modifier ensuite.

Dans une architecture Docker

Nos sites tournent dans des conteneurs, et ajouter un cron à l'intérieur du conteneur applicatif mélange deux responsabilités. Nous utilisons un conteneur dédié, fondé sur l'image officielle wordpress:cli, qui partage les fichiers et la configuration du site :

wp-cron:
  image: wordpress:cli
  user: "33:33"
  working_dir: /var/www/html
  mem_limit: 256m
  volumes:
    - ./data/:/var/www/html/
    - ./conf/prod/wp-config.php:/var/www/html/wp-config.php
  command: >
    sh -c "while true; do
      wp cron event run --due-now --path=/var/www/html --url=atari.greenlog.fr;
      wp cron event run --due-now --path=/var/www/html --url=mon-controle-technique.greenlog.fr;
      sleep 300;
    done"

Quelques choix méritent explication :

L'avantage principal est l'isolement : les tâches ne consomment aucun processus du serveur web, et un traitement long n'occupe jamais un worker qui devrait servir des visiteurs.

Ne pas rendre les échecs silencieux

Notre première version de ce conteneur ajoutait 2>/dev/null || true à chaque commande, pour qu'une erreur ne fasse pas sortir la boucle. L'intention est bonne, l'effet l'est moins : une extension en erreur fatale à chaque exécution ne laisse alors aucune trace. C'est la faiblesse de ce montage, et la première chose à changer si vous le reprenez :

Vérifier que tout tourne

Trois commandes suffisent à contrôler l'état des tâches d'un site :

# Tâches programmées et prochaine échéance
wp cron event list --url=mon-controle-technique.greenlog.fr

# Test du mécanisme lui-même
wp cron test --url=mon-controle-technique.greenlog.fr

# Journaux du conteneur dédié
docker compose logs --tail=50 wp-cron

Le signe d'un problème est simple à repérer : des tâches dont l'échéance, dans wp cron event list, est dépassée depuis longtemps. Avec un déclenchement toutes les cinq minutes, aucune tâche ne devrait être en retard de plus de quelques minutes.

Notez que wp cron test vérifie la capacité de WordPress à s'appeler lui-même en HTTP — le mécanisme natif. Avec DISABLE_WP_CRON, il peut signaler un avertissement sans conséquence : c'est la liste des événements qui fait foi.

Les erreurs fréquentes

Les tâches longues : prévoir aussi une commande

Dernier conseil, tiré de notre pratique. Nos sitemaps de villes et de centres, qui représentent des milliers d'URL, sont reconstruits chaque nuit par un événement WP-Cron programmé vers 3 h 30. C'est précisément le genre de tâche que le déclenchement par les visites servait mal : longue, lourde, et dont l'heure importe. Exécutée par WP-CLI dans le conteneur dédié, elle échappe au délai d'expiration du serveur web.

Nous l'avons aussi exposée comme une commande WP-CLI à part entière (wp pct sitemaps build). Cela permet de relancer une reconstruction à la main après un incident, sans attendre la nuit suivante ni bricoler l'échéance de l'événement. Toute tâche planifiée qui dure plus de quelques secondes gagne à avoir cette double entrée : l'événement pour la régularité, la commande pour l'exploitation. Et l'index des sitemaps ne doit jamais prétendre qu'une reconstruction a eu lieu si la tâche n'a pas tourné.

Le même raisonnement vaut côté Laravel, où nous avons rencontré un conteneur de planification qui ne planifiait rien : voir les tâches planifiées Laravel sous Docker.