Programmer un article ne règle pas un réveil sur le serveur. WordPress note une tâche, « publier l’article 123 à telle heure », dans une option de la base, et ne l’exécute que lorsqu’une requête PHP passe après l’heure dite et parvient à appeler wp-cron.php. Pas de requête, ou un appel qui n’aboutit pas, et l’article attend. Tout ce qui suit est lu dans la branche 7.1 du code de WordPress, celle de la version 7.1.4.
Lancez wp cron test : il dit si WordPress arrive à s’appeler lui-même. Publiez les articles en retard avec wp cron event run --due-now. Pour que cela ne se reproduise pas, ajoutez define( 'DISABLE_WP_CRON', true ); dans wp-config.php et confiez wp-cron.php à une vraie tâche cron du serveur, toutes les cinq minutes.
Ce que l’écran affiche, et ce qu’il calcule
« Planification manquée » n’est pas un statut. En base, l’article garde le statut future, affiché « Planifié ». Le libellé rouge est calculé à chaque affichage par WP_Posts_List_Table::column_date(), dans wp-admin/includes/class-wp-posts-list-table.php : si le statut est future et que la date de l’article est passée, même d’une seconde, la colonne Date affiche « Planification manquée », traduction de Missed schedule dans le fichier fr_FR de la version 7.1.4. Sinon, « Planifié ».
Ce calcul part de post_date, l’heure locale de l’article, relue avec le fuseau horaire réglé aujourd’hui. La publication, elle, part de post_date_gmt. La différence compte pour la cause 5.
Le mécanisme, lu dans le code
À l’enregistrement. Quand un article passe au statut future, wp_transition_post_status() déclenche l’action future_post (ou future_page, un nom par type de contenu). WP_Post_Type::add_hooks() y a branché _future_post_hook(), dans wp-includes/post.php, qui inscrit un événement unique publish_future_post, avec l’identifiant de l’article pour argument, à l’heure UTC de sa date. Toutes ces tâches tiennent dans une seule option, cron.
À l’heure dite, rien. Aucun processus n’attend cette heure. default-filters.php branche wp_cron() sur init, donc sur chaque requête qui charge WordPress. En 7.1, wp_cron() reporte le travail à la fin de la requête, sur shutdown. Là, _wp_cron() sort tout de suite si DISABLE_WP_CRON vaut vrai ; sinon, si au moins un événement est échu, il appelle spawn_cron().
L’appel à soi-même. spawn_cron(), dans wp-includes/cron.php, pose un verrou (le transient doing_cron, qui empêche un second lancement pendant WP_CRON_LOCK_TIMEOUT, 60 secondes par défaut), puis envoie une requête POST à site_url( 'wp-cron.php' ) avec un délai de 0,01 seconde et sans attendre la réponse. C’est ce second processus, wp-cron.php, qui exécute les tâches échues.
La publication. L’événement appelle check_and_publish_future_post(). La fonction vérifie que l’article est toujours future, compare post_date_gmt à time() : si l’heure n’est pas encore venue, elle reprogramme l’événement et s’arrête. Sinon, elle appelle wp_publish_post(), qui passe le statut à publish.
Deux maillons, donc : une requête qui charge WordPress après l’heure, et une requête du serveur vers lui-même qui aboutit. Chaque cause casse l’un des deux.
Diagnostiquer en quatre commandes
Les articles et pages réellement en retard, selon l’horloge de la base :
SELECT ID, post_type, post_title, post_date, post_date_gmt
FROM wp_posts
WHERE post_status = 'future' AND post_date_gmt < UTC_TIMESTAMP();Le préfixe wp_ est celui de $table_prefix dans wp-config.php. Avec WP-CLI, les mêmes articles, puis leurs événements :
wp post list --post_status=future --fields=ID,post_title,post_date,post_date_gmt
wp cron event list --hook=publish_future_post --fields=hook,next_run_gmt,args
wp cron test- L’événement existe, avec une date passée. La tâche est bien inscrite, personne ne l’a lancée. Causes 1 à 4.
- L’article est en retard mais n’a aucun événement. Rien ne le publiera jamais. Cause 6.
wp cron testrépond The DISABLE_WP_CRON constant is set to true. Cause 3.- Il répond WP-Cron spawn failed with error ou returned HTTP status code suivi d’un code autre que 200 : l’appel à soi-même échoue. Cause 4.
Sans terminal, Outils → Santé du site fait un test voisin. WP_Site_Health::get_test_scheduled_events() affiche « Un évènement planifié a échoué » dès qu’une tâche a plus de cinq minutes de retard, et « Un évènement planifié est en retard » pour un retard plus court. Avec DISABLE_WP_CRON, les seuils passent à une heure et à quinze minutes. Le même écran teste la requête de bouclage, avec un piège décrit à la cause 4.
Les causes, par ordre de fréquence
1. Personne n’est passé
Un site peu visité, un article programmé à 6 h, la première visite à 9 h : il paraît à 9 h, pas à 6 h. Ce n’est pas une panne, c’est le fonctionnement prévu. Signature : l’article finit par paraître, en retard, et la date de publication reste celle prévue. Réparation : une tâche cron système, plus bas.
2. Un cache de page répond à la place de WordPress
Un cache qui sert du HTML déjà prêt, qu’il soit au niveau du serveur, d’un CDN ou d’une extension qui court-circuite PHP, ne charge pas WordPress. Pas de chargement, pas d’init, pas de wp_cron(). Un site très visité peut ainsi n’exécuter son cron qu’aux rares requêtes non mises en cache : administration, formulaires, pages nouvelles. Le banc d’essai des extensions de cache montre lesquelles servent les pages sans PHP selon le serveur. Signature : les retards se règlent dès qu’un administrateur ouvre le tableau de bord. Réparation : la tâche cron système, qui ne dépend plus des visites.
3. DISABLE_WP_CRON sans tâche cron derrière
Beaucoup de guides de performance conseillent define( 'DISABLE_WP_CRON', true );. La constante coupe le déclenchement par les visites, rien de plus : _wp_cron() s’arrête à sa première ligne. Si personne n’a créé la tâche du serveur qui doit prendre le relais, plus rien ne s’exécute : ni les publications, ni les mises à jour automatiques. Signature : tous les articles programmés sont en retard, et wp cron event list montre de nombreuses tâches aux dates passées. Réparation : créer la tâche, ou retirer la constante.
4. Le serveur n’arrive pas à s’appeler lui-même
La requête de spawn_cron() part du serveur vers l’adresse du site. Elle échoue si le nom de domaine ne résout pas vers ce serveur depuis lui-même, si un pare-feu ou une extension de sécurité bloque wp-cron.php, ou si le site est protégé par un mot de passe HTTP : spawn_cron() n’envoie aucun en-tête Authorization. Comme la requête n’attend pas de réponse, l’échec est silencieux.
Le piège de Santé du site : can_perform_loopback() appelle bien wp-cron.php, mais avec les cookies de l’administrateur et, s’il est connecté par mot de passe HTTP, ses identifiants. Le test peut donc passer alors que le vrai déclenchement échoue. wp cron test, qui imite spawn_cron() mais attend la réponse trois secondes, est plus fidèle. Réparation : autoriser l’adresse du serveur à joindre wp-cron.php, ou passer à une tâche cron système, qui ne dépend pas de cet appel.
5. L’heure n’est pas celle que vous croyez
WordPress force PHP en UTC (wp-settings.php) et compare tout à time(), donc à l’horloge du serveur. Une horloge système fausse décale toutes les publications d’autant. Autre cas : changer le fuseau horaire dans Réglages → Général après avoir programmé. Les deux dates de l’article ne bougent pas, mais la liste relit post_date avec le nouveau fuseau : elle peut annoncer une planification manquée qui n’a pas encore eu lieu, ou l’inverse. Signature : l’écart entre l’heure attendue et l’heure réelle est un nombre rond d’heures. Vérification : comparez post_date_gmt à UTC_TIMESTAMP(), pas à la liste.
6. L’événement n’existe pas
Seul le passage par wp_transition_post_status() inscrit la tâche. Un article mis en future par une requête SQL directe, ou par un outil d’import qui écrit dans la table sans passer par wp_insert_post(), n’a jamais eu d’événement. Autre possibilité, plus rare : l’option cron est relue puis réécrite en entier à chaque ajout par _set_cron_array(), et deux écritures simultanées peuvent en perdre une. Signature : l’article est dans la requête SQL, pas dans wp cron event list. Réparation : le publier à la main, ci-dessous.
Publier les articles déjà en retard
Si l’événement existe, exécutez simplement ce qui est échu :
wp cron event run --due-nowS’il n’existe pas, publiez l’article directement, en remplaçant 123 par son identifiant :
wp post update 123 --post_status=publishDans les deux cas, l’article garde la date prévue : un article de 6 h publié à 11 h se range sous ceux de 7 h à 10 h, et peut manquer le haut de la page d’accueil ou du flux RSS. Si vous voulez qu’il paraisse comme neuf, donnez-lui une nouvelle date.
Sans terminal, Mes Publications planifiées liste, dans Outils → Mes Publications planifiées, les articles et pages encore planifiés dont l’heure est passée. Un bouton, sur votre décision, les reprogramme dans cinq minutes. L’extension ne publie rien d’elle-même et ne touche pas au cron du serveur : la reprogrammation a besoin d’un déclencheur qui fonctionne, ce qu’apporte la section suivante.
Éviter que cela recommence : une vraie tâche cron
Deux étapes, dans cet ordre. D’abord, dans wp-config.php, avant la ligne qui charge wp-settings.php :
define( 'DISABLE_WP_CRON', true );Puis une tâche du serveur, toutes les cinq minutes, depuis la crontab du compte d’hébergement ou le panneau de l’hébergeur. Par HTTP :
*/5 * * * * curl -s -o /dev/null https://votre-site.fr/wp-cron.phpLa requête doit rester un GET : wp-cron.php s’arrête dès que $_POST n’est pas vide. Sans paramètre doing_wp_cron, il pose lui-même son verrou, et refuse de tourner si un autre passage date de moins de 60 secondes. Par WP-CLI, sans passer par le réseau :
*/5 * * * * wp --path=/chemin/vers/le/site cron event run --due-now --quietL’environnement de cron a un PATH réduit : donnez le chemin complet de wp si la tâche ne le trouve pas. Avec DISABLE_WP_CRON, Santé du site attend quinze minutes de retard avant de signaler une tâche en retard, et une heure avant de la dire en échec : une tâche toutes les cinq minutes reste dans les clous.
Si l’hébergement n’offre aucune tâche cron et que l’appel à soi-même est bloqué, il reste define( 'ALTERNATE_WP_CRON', true ); : le déclenchement se fait par une redirection du visiteur vers la même adresse suivie de ?doing_wp_cron=, sur les seules requêtes GET. Ce paramètre apparaît alors dans la barre d’adresse. C’est un dernier recours, et il dépend toujours des visites.
Le même cron fait tourner les mises à jour automatiques. Un site où les publications planifiées échouent ne se met probablement pas à jour seul non plus ; une mise à jour du cœur interrompue laisse, elle, un verrou de quinze minutes.
Vérifier que c’est terminé
Programmez un brouillon à cinq minutes, pas moins : wp_insert_post() publie immédiatement un article dont l’heure prévue est à moins d’une minute. Si vous avez posé la tâche cron système, ne visitez ni le site ni l’administration entre-temps : c’est elle que vous testez. Puis relancez la requête SQL du diagnostic. Attendu : aucune ligne, l’article en publish, et wp cron event list sans tâche aux dates passées. Santé du site affiche alors « Les évènements planifiés fonctionnent ».