Un client nous a contactés parce que son site d’annonces immobilières envoyait le même e-mail récapitulatif à ses agents jusqu’à quarante fois par jour, au lieu d’une seule fois comme prévu. Aucune erreur dans les journaux, aucun plugin de spam suspect. La cause était plus banale : une extension maison programmait une tâche cron à chaque chargement d’une page d’administration précise, sans jamais vérifier si cette tâche existait déjà.
Ce bug se répète d’un projet à l’autre depuis des années, tant l’erreur est facile à commettre. Elle mérite qu’on s’y attarde une bonne fois, avec le correctif exact et les raisons pour lesquelles WP-Cron se comporte ainsi par nature.
Symptôme : des événements qui s’accumulent
La commande suivante, exécutée sur le site du client, a révélé l’ampleur du problème :
wp cron event list --fields=hook,next_run_relative,schedule \
--format=table | grep envoi_recap_agents
Le résultat affichait quarante-sept lignes identiques pour le hook envoi_recap_agents, chacune programmée à quelques secondes d’intervalle les unes des autres. WordPress ne fusionne jamais deux événements cron portant le même nom si leurs arguments diffèrent, même légèrement, et sur ce site, un identifiant de session généré à chaque appel s’était glissé par erreur dans les arguments de la tâche.
Diagnostic : l’appel fautif
Le code incriminé ressemblait à ceci, placé directement dans le rendu d’un écran d’administration, donc exécuté à chaque chargement de cette page par n’importe quel agent connecté :
function afficher_ecran_agents() {
// ... rendu de l'écran ...
wp_schedule_event( time(), 'daily', 'envoi_recap_agents', array( uniqid() ) );
}
add_action( 'admin_init', 'afficher_ecran_agents' );
Deux fautes se cumulent ici. D’abord, l’appel est placé dans admin_init, donc rejoué à chaque affichage de la page, sans aucune condition. Ensuite, l’argument uniqid() change à chaque appel, ce qui empêche WordPress de reconnaître qu’une tâche identique existe déjà, puisque la comparaison porte sur le hook et sur les arguments combinés.
Le correctif : wp_next_scheduled avant toute programmation
La règle à retenir est simple et devrait précéder systématiquement tout appel à wp_schedule_event() : vérifier d’abord qu’aucun événement identique n’est déjà programmé, avec des arguments strictement identiques.

function afficher_ecran_agents() {
// ... rendu de l'écran ...
if ( ! wp_next_scheduled( 'envoi_recap_agents' ) ) {
wp_schedule_event( time(), 'daily', 'envoi_recap_agents' );
}
}
add_action( 'admin_init', 'afficher_ecran_agents' );
Deux changements corrigent le problème : la suppression de l’argument variable inutile, et la vérification via wp_next_scheduled(), qui recherche un événement portant exactement le même hook et les mêmes arguments. Sans argument du tout, cette vérification devient fiable et stable dans le temps.
Pourquoi ne pas programmer à l’activation seulement ?
La meilleure pratique reste de programmer la tâche une seule fois, lors de l’activation de l’extension via register_activation_hook(), et de la déprogrammer proprement à la désactivation :
register_activation_hook( __FILE__, function() {
if ( ! wp_next_scheduled( 'envoi_recap_agents' ) ) {
wp_schedule_event( time(), 'daily', 'envoi_recap_agents' );
}
} );
register_deactivation_hook( __FILE__, function() {
$timestamp = wp_next_scheduled( 'envoi_recap_agents' );
if ( $timestamp ) {
wp_unschedule_event( $timestamp, 'envoi_recap_agents' );
}
} );
Cette approche évite complètement le besoin de vérifier à chaque chargement de page : la tâche est programmée une fois pour toutes, et le hook wp_unschedule_event() nettoie proprement lors de la désactivation, ce qui évite aussi les tâches fantômes qui continuent de tourner après la suppression d’une extension.
Nettoyer les tâches déjà accumulées
Sur un site déjà touché, corriger le code ne suffit pas : les événements déjà programmés restent en base tant qu’on ne les supprime pas explicitement. La commande WP-CLI suivante supprime toutes les occurrences d’un hook donné :
wp cron event delete envoi_recap_agents
Il faut la répéter jusqu’à ce que wp cron event list ne renvoie plus aucune ligne pour ce hook, car chaque appel ne supprime qu’une seule occurrence à la fois lorsque les arguments diffèrent d’un événement à l’autre.
- Toujours encadrer
wp_schedule_event()d’un testwp_next_scheduled(), sans exception, même pour un prototype rapide. - Programmer la tâche à l’activation de l’extension plutôt que dans un hook rejoué à chaque page.
- Éviter les arguments variables dans les tâches récurrentes, sauf besoin réel et documenté, car ils cassent la détection de doublon.
Un rappel qui vaut pour toute extension : WP-Cron n’est pas un vrai ordonnanceur système, c’est un mécanisme déclenché par le trafic du site. Une tâche mal gérée ne se contente pas de ne pas s’exécuter à l’heure prévue, elle peut s’exécuter bien trop souvent.
Prévention
Ce type de bug se détecte tôt avec une simple habitude de revue de code : chercher systématiquement les occurrences de wp_schedule_event dans une extension et vérifier qu’aucune ne s’exécute en dehors d’un hook d’activation ou d’une garde explicite. Un test automatisé simple, qui appelle deux fois de suite la fonction d’enregistrement et vérifie qu’un seul événement existe ensuite, suffit à éviter que ce piège ne revienne dans une future version de l’extension.