L'API Cron de WordPress porte un nom trompeur. Ce n'est pas un planificateur au sens où l'entend un administrateur système : rien ne s'exécute à heure fixe, rien ne tourne en arrière-plan, et rien ne se déclenche si personne ne visite le site. WordPress se contente de tenir une liste d'échéances et de la consulter à chaque chargement de page ; si une échéance est dépassée, la tâche correspondante part immédiatement, dans le processus du visiteur. Cette mécanique explique la totalité des comportements déroutants qu'on lui reproche, des tâches qui ne partent jamais sur un site sans trafic à celles qui partent trois fois de suite sur un site très fréquenté. Comprendre ce fonctionnement est le préalable à toute tâche récurrente écrite proprement, et il évite surtout de chercher pendant des heures une erreur de code là où le problème est ailleurs : dans le trafic du site, dans sa capacité à s'appeler lui-même, ou dans une échéance programmée quarante fois par un plugin mal écrit.

Ce que fait réellement le cron de WordPress

Le cœur du mécanisme tient en peu de choses. WordPress conserve dans ses options un tableau d'échéances, chacune associant un horodatage, le nom d'un crochet, une périodicité et d'éventuels arguments. À chaque chargement de page, une fonction parcourt ce tableau et compare les horodatages à l'heure courante. Si une échéance est dépassée, WordPress déclenche une requête HTTP vers son propre fichier dédié, en la relâchant sans attendre la réponse, et c'est cette seconde requête qui exécute réellement la tâche. Ce détour évite que le visiteur attende la fin d'un traitement long, mais il introduit une dépendance à laquelle personne ne pense : le site doit pouvoir s'appeler lui-même. Sur un serveur derrière un pare-feu applicatif, une authentification HTTP, un certificat auto-signé ou un enregistrement DNS interne différent de l'externe, cette requête échoue silencieusement, et le cron cesse de fonctionner sans qu'aucun message ne le signale. C'est de loin la première cause de tâches qui ne partent jamais, très loin devant les erreurs de code. Le diagnostic est heureusement simple : il suffit de demander au serveur, depuis lui-même, l'adresse publique du site, et de regarder ce qui revient. Une réponse correcte élimine la piste ; un délai d'attente, une erreur de certificat ou une redirection vers une page d'authentification la confirment en quelques secondes. Le second effet de cette architecture concerne la précision : une tâche programmée à trois heures du matin sur un site qui ne reçoit aucune visite entre minuit et huit heures s'exécutera à huit heures. Elle n'est pas perdue, elle est simplement en retard, et ce retard s'accumule sur les tâches horaires, qui finissent par se déclencher en rafale au premier visiteur de la journée. Toute logique métier qui suppose une exécution à l'heure dite doit donc être écrite en tenant compte de l'heure réelle d'exécution, jamais de l'heure théorique. Nous détaillons ces réglages dans notre rubrique administration de WordPress, où la même règle revient systématiquement : ce qui dépend du passage d'un visiteur ne peut pas être garanti.

Déclenchement d’une tâche planifiée WordPress au passage d’un visiteur

Enregistrer une tâche au bon endroit et au bon moment

L'erreur la plus répandue consiste à appeler la fonction de programmation directement dans le fichier de fonctions du thème ou dans le corps d'un plugin. Le code s'exécute alors à chaque chargement de page, et bien qu'une vérification préalable empêche en théorie les doublons, la moindre différence dans les arguments crée une seconde entrée, puis une troisième. Nous avons repris des sites où la même tâche figurait quarante fois dans la liste des échéances, chacune se déclenchant séparément. La règle est de programmer une seule fois, au moment de l'activation du plugin ou du thème, à l'aide du crochet prévu pour cela, et de vérifier auparavant qu'aucune échéance n'existe déjà pour ce crochet. Cette vérification renvoie l'horodatage de la prochaine occurrence si elle existe, et une valeur vide sinon : c'est exactement ce qu'il faut tester. La contrepartie est qu'une tâche ajoutée après coup, sur un plugin déjà actif, ne sera jamais programmée tant que le plugin ne sera pas désactivé puis réactivé, ce qui déroute au premier essai. La parade consiste à conserver un numéro de version dans les options et à reprogrammer lorsqu'il change, mécanisme qui sert de toute façon aux migrations de données. Il faut ensuite associer une fonction au crochet, et cette association doit être déclarée à chaque chargement, sans condition : le crochet est consulté au moment de l'exécution, dans un contexte où le plugin est chargé mais où rien ne rejoue la logique d'activation. Un crochet sans fonction associée produit le comportement le plus déroutant de tous, une échéance qui disparaît de la liste sans que rien ne se passe, WordPress considérant la tâche comme accomplie. Enfin, la périodicité doit exister : les trois valeurs fournies d'origine couvrent l'heure, deux fois par jour et le jour, et toute autre valeur suppose de l'ajouter par un filtre dédié, faute de quoi la programmation échoue sans erreur visible. Vous trouverez la mécanique complète dans notre tutoriel sur la création et la modification d'une tâche Cron WordPress.

Symptôme observé Cause la plus fréquente
La tâche ne part jamais Le site ne peut pas s'appeler lui-même en HTTP
La tâche part plusieurs fois de suite Doublons d'échéances créés à chaque chargement de page
L'échéance disparaît sans effet Aucune fonction associée au crochet au moment de l'exécution
La programmation échoue en silence Périodicité personnalisée non déclarée par un filtre
Exécution systématiquement en retard Trafic trop faible aux heures visées
Tâche fantôme après suppression du plugin Absence de désinscription à la désactivation

Écrire une tâche qui ne casse pas le site

Une tâche planifiée s'exécute dans un contexte particulier qu'il faut avoir en tête : aucun utilisateur n'est connecté, aucune sortie ne doit être produite, et le temps d'exécution est celui du serveur, souvent limité à trente secondes. Trois conséquences pratiques en découlent. La première est qu'aucune fonction dépendant des droits de l'utilisateur courant ne peut être appelée, puisqu'il n'y en a pas ; les vérifications de capacités renverront toujours faux, et une tâche qui en dépend ne fera rien. La deuxième est qu'un affichage, même un simple message de débogage, part dans la réponse HTTP de la requête interne et peut, selon le moment, corrompre un en-tête ou polluer un flux. Tout retour d'information passe donc par la journalisation, jamais par un affichage. La troisième est que le traitement doit être découpé. Une tâche qui parcourt dix mille articles échouera au bout de trente secondes, laissant le travail à moitié fait, et repartira de zéro à l'occurrence suivante, indéfiniment. Le découpage consiste à traiter un lot de taille fixe, à mémoriser la position atteinte dans une option, et à reprogrammer immédiatement une occurrence à brève échéance tant qu'il reste du travail. Ce schéma, dit de traitement par lots relancé, transforme une tâche impossible en une série d'exécutions courtes, chacune sans risque. Il impose en revanche une discipline : la position mémorisée doit être remise à zéro en cas d'interruption, et le lot doit être assez petit pour tenir dans le temps imparti même sur le serveur le plus lent du parc. Une tâche doit également être idempotente, c'est-à-dire produire le même résultat qu'elle soit exécutée une ou trois fois, puisque nous avons vu que les doublons et les rafales sont fréquents. En pratique, cela signifie vérifier l'état avant d'agir plutôt que supposer un état de départ, et poser un verrou en début d'exécution, sous la forme d'un transient de courte durée, que la tâche libère en sortant. Sans ce verrou, deux visiteurs arrivant à la même seconde déclenchent deux exécutions parallèles qui se marchent dessus, et le résultat dépend de laquelle termine la dernière. La durée du verrou mérite réflexion : trop courte, elle laisse passer une seconde exécution alors que la première n'est pas terminée ; trop longue, elle bloque définitivement la tâche si le processus meurt avant de la libérer, ce qui arrive à chaque dépassement de temps d'exécution. Une durée légèrement supérieure au temps de traitement observé, doublée d'une libération dans un bloc exécuté quoi qu'il arrive, couvre les deux risques. Enfin, il faut se souvenir que ces tâches s'exécutent avec les droits du processus web : tout ce qu'elles écrivent appartient à cet utilisateur, et un fichier créé par une tâche planifiée peut parfaitement devenir illisible pour un déploiement effectué sous un autre compte.

Retard d'exécution d'une tâche horaire selon le trafic du site
Site à fort trafic, plus de 500 visites par jour
3 min
Site à trafic moyen, 50 à 500 visites
22 min
Site à faible trafic, moins de 50 visites
1 h 35
Site vitrine sans trafic la nuit
jusqu'à 8 h
Cron serveur toutes les 5 minutes
5 min

Écart médian entre l'heure théorique et l'heure réelle d'exécution. Le déclenchement natif dépend entièrement du passage d'un visiteur, ce que seul un planificateur système corrige.

Déclarer une périodicité qui n'existe pas d'origine

WordPress ne connaît que trois intervalles au départ : toutes les heures, deux fois par jour et une fois par jour. Toute autre valeur doit être déclarée par un filtre, et c'est une source d'échecs silencieux parce que la fonction de programmation ne proteste pas lorsqu'on lui passe un nom d'intervalle inconnu : elle renvoie simplement un échec que personne ne teste. La déclaration consiste à ajouter une entrée à un tableau, avec un identifiant, une durée en secondes et un libellé destiné à l'affichage. Deux précautions valent d'être connues. La première est que ce filtre doit être déclaré avant toute tentative de programmation, donc à chaque chargement et non dans la logique d'activation, sans quoi l'enregistrement du plugin échoue précisément au moment où il en aurait besoin. La seconde touche à l'intervalle lui-même : descendre en dessous de cinq minutes n'a pas de sens avec le déclenchement natif, puisque la précision réelle dépend du trafic, et cela ne fait qu'allonger la liste des échéances dépassées. Sur un site à faible fréquentation, une tâche déclarée toutes les cinq minutes ne s'exécutera pas douze fois par heure mais une seule fois, au premier visiteur, en ayant accumulé onze retards. Choisir un intervalle honnête, c'est-à-dire compatible avec le trafic réel, évite ce genre de faux confort. La question à se poser n'est pas « à quelle fréquence voudrais-je que cela tourne » mais « quel retard maximal puis-je accepter », car c'est le retard, et non la périodicité déclarée, qui décrit ce qui se passera réellement sur le site. Il faut enfin savoir que la modification d'un intervalle ne s'applique pas aux échéances déjà programmées : elles conservent la périodicité enregistrée au moment de leur création. Changer la durée dans le filtre sans reprogrammer donne l'impression que le changement n'a servi à rien, alors qu'il attend simplement la prochaine programmation pour prendre effet.

Tester une tâche sans attendre son échéance

Une tâche récurrente écrite le matin ne se teste pas en attendant le lendemain. Trois méthodes permettent de la déclencher immédiatement, et elles ne se valent pas. La plus simple consiste à appeler directement la fonction associée au crochet depuis un script temporaire : cela vérifie le code métier, mais absolument pas la mécanique de planification, ce qui laisse passer les erreurs les plus fréquentes. La deuxième consiste à déclencher le crochet lui-même, ce qui exécute toutes les fonctions qui y sont associées et valide donc l'association, sans pour autant tester l'échéance. La troisième, la seule complète, passe par la ligne de commande : elle permet de lister les échéances, d'en déclencher une nommément, et d'exécuter toutes celles qui sont dues, exactement comme le ferait un visiteur. C'est la méthode à privilégier, parce qu'elle emprunte le chemin réel et révèle les problèmes de contexte, notamment l'absence d'utilisateur connecté et les limites de temps d'exécution. Un dernier réflexe évite beaucoup de perte de temps : avant de conclure qu'une tâche ne fonctionne pas, il faut vérifier qu'elle est bien programmée. La confusion entre une tâche absente de la liste et une tâche présente mais jamais déclenchée oriente vers deux diagnostics opposés, l'un dans le code d'enregistrement, l'autre dans la capacité du site à s'appeler lui-même. Regarder la liste avant de modifier quoi que ce soit départage les deux en dix secondes. Sur un site en production, ce contrôle gagne à être automatisé sous la forme d'une alerte : si le crochet attendu n'apparaît plus dans la liste, ou si sa prochaine occurrence est dépassée depuis plus d'une journée, un courriel part. C'est le seul moyen de savoir qu'une tâche a cessé de fonctionner, puisque par construction son échec ne produit aucun symptôme visible.

Désinscrire, surveiller, et basculer vers un vrai cron

Une tâche programmée survit à la désactivation du plugin qui l'a créée, et même à sa suppression. L'échéance reste dans les options, WordPress la déclenche, ne trouve aucune fonction associée, et la retire ou la reprogramme selon les cas. Ces tâches fantômes s'accumulent au fil des années et alourdissent une option lue à chaque chargement de page. La désinscription au moment de la désactivation est donc obligatoire, et elle demande de retirer l'échéance exactement avec les mêmes arguments que ceux utilisés à la programmation, sans quoi elle ne correspond à rien et l'appel est sans effet. Pour la surveillance, la première chose à savoir est que la liste des échéances est lisible : elle se consulte en ligne de commande, ce qui donne en une commande la liste des crochets, leur prochaine occurrence et leur périodicité. C'est le premier geste sur un site dont les tâches semblent capricieuses, et il révèle immédiatement les doublons, les tâches fantômes et les échéances dépassées depuis des semaines, signe certain que le déclenchement HTTP ne fonctionne plus. Reste la question du remplacement. Dès qu'une tâche doit s'exécuter à heure fixe, ou dès que le site reçoit trop peu de trafic pour garantir un déclenchement, la bonne pratique consiste à désactiver le déclenchement automatique par une constante dans le fichier de configuration, puis à appeler le fichier dédié depuis un véritable planificateur système, toutes les cinq à quinze minutes. Le mécanisme interne de WordPress continue de fonctionner à l'identique, seul le déclencheur change, et il devient fiable. Attention à un piège classique : désactiver le déclenchement automatique sans mettre en place le planificateur externe arrête toutes les tâches du site, y compris les publications planifiées et les mises à jour automatiques, sans le moindre avertissement. Le fichier appelé par ce planificateur est celui-là même que nous décrivons dans notre article sur le fichier wp-cron.php de WordPress, et il accepte d'être appelé en ligne de commande comme en HTTP. L'appel en ligne de commande est préférable lorsqu'il est possible : il évite d'ouvrir une connexion réseau, contourne les pare-feu applicatifs, et bénéficie généralement de limites de temps d'exécution plus généreuses que celles appliquées aux requêtes web. Sur un hébergement mutualisé où seule la planification d'une URL est proposée, l'appel HTTP reste parfaitement valable, à condition de vérifier une fois que la réponse obtenue est bien celle du fichier et non une page d'erreur ou une redirection. Cette vérification se refait après chaque changement d'hébergement, la réponse obtenue dépendant de la configuration du serveur autant que du site, et un appel qui fonctionnait parfaitement pouvant se mettre à renvoyer une page d'accueil sans prévenir.