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 :
user: "33:33": l'identifiant dewww-datadans les images Debian. Le conteneur écrit avec les mêmes droits que le serveur web.- Une commande par site : en multisite, WP-CLI n'agit que sur le site désigné par
--url. Une seule commande sans--urlne traiterait que le site principal. Ce point est détaillé dans notre guide sur le multisite à domaines distincts. - Une limite mémoire : une tâche d'extension qui fuit ne peut pas entraîner le reste du serveur.
- Cinq minutes : nos tâches les plus fréquentes sont horaires ; cinq minutes de latence au maximum sont sans conséquence.
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 :
- garder
|| truepour la robustesse de la boucle, mais laisser la sortie d'erreur aller dans les journaux du conteneur, déjà limités en taille par la configuration de journalisation de Docker ; - préfixer chaque exécution d'un horodatage et du site traité, pour que les journaux se lisent sans deviner.
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
- Exécuter WP-CLI en root. Les fichiers créés pendant une tâche (caches, sitemaps, miniatures) appartiennent alors à root, et le serveur web ne peut plus les réécrire. C'est pourquoi notre conteneur de tâches tourne avec l'utilisateur 33:33, celui du serveur web de l'image officielle.
- Oublier un site du réseau. Sur un multisite, un appel sans
--urlne traite que le site principal. Chaque nouveau site doit être ajouté à la boucle, sinon ses publications programmées ne partiront jamais. - Laisser des exécutions se chevaucher. Si une tâche dure plus longtemps que l'intervalle de déclenchement, deux exécutions peuvent tourner en parallèle. Une boucle séquentielle, comme la nôtre, l'évite par construction : l'appel suivant n'est lancé qu'après la fin du précédent et la pause.
- Confondre fuseaux horaires. Les échéances de WP-Cron sont stockées en horodatage UTC, alors que l'administration affiche l'heure du site. Une tâche « en retard d'une heure » n'est souvent qu'une lecture dans le mauvais fuseau.
- Ne pas borner la mémoire. Une tâche qui s'emballe ne doit pas priver le site de ressources : une limite propre au conteneur de tâches (256 Mo chez nous) la cantonne.
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.