Le tableau des actions planifiées affiche plus de mille quatre cents occurrences de la même tâche, toutes marquées « en attente », toutes créées automatiquement les unes après les autres sur plusieurs jours. Aucune erreur fatale dans les journaux, aucune alerte du système de surveillance. C’est exactement ce type de symptôme discret qui rend ce genre de panne difficile à repérer avant qu’il ne sature la file d’attente.
Ce billet ne traite pas WP-Cron natif, mais Action Scheduler, la bibliothèque utilisée par de nombreuses extensions pour gérer des tâches en arrière-plan avec un système de files nommées et de reprise après échec.
Symptôme : une action qui se recrée sans jamais aboutir
L’écran Outils > Actions planifiées montre la même action, portant le même identifiant de groupe, réapparaître régulièrement dans l’état « en attente », alors qu’aucune tâche de ce type ne devrait s’exécuter aussi fréquemment selon la logique métier de l’extension. Le nombre d’occurrences grossit plus vite qu’il ne se résorbe, signe que chaque exécution recrée une nouvelle tâche identique plutôt que de simplement terminer ou échouer proprement.
Diagnostic : retrouver le callback qui reprogramme sans condition

La cause la plus fréquente de ce comportement se trouve dans le callback lui-même, lorsqu’il englobe son traitement dans un bloc try/catch qui avale l’exception sans la relancer, tout en reprogrammant systématiquement une nouvelle tentative dans son bloc finally — sans jamais vérifier combien de tentatives ont déjà échoué.
// Code fautif observé en production
add_action( 'synchroniser_commande_erp', function ( $commande_id ) {
try {
appeler_api_erp( $commande_id );
} catch ( Exception $e ) {
error_log( 'Erreur ERP : ' . $e->getMessage() );
// L'exception est avalée, rien ne remonte
} finally {
as_schedule_single_action( time() + 300, 'synchroniser_commande_erp', [ $commande_id ] );
}
} );
Le bloc finally s’exécute qu’il y ait eu une exception ou non, et reprogramme systématiquement une nouvelle tentative, cinq minutes plus tard, sans jamais incrémenter ni consulter un compteur de tentatives. Si l’API distante reste indisponible plusieurs jours, la file continue de grossir indéfiniment, chaque tentative créant la suivante avant même d’avoir constaté son propre échec.
Correctif : borner les tentatives et laisser Action Scheduler gérer l’échec
Action Scheduler dispose déjà d’un mécanisme de suivi des échecs via le hook action_scheduler_failed_action, qui se déclenche lorsqu’une exception non interceptée remonte hors du callback. La correction consiste à laisser l’exception remonter, plutôt que de la neutraliser, et à borner explicitement le nombre de nouvelles tentatives via une métadonnée de comptage.
add_action( 'synchroniser_commande_erp', function ( $commande_id ) {
$tentatives = (int) get_transient( 'tentatives_sync_' . $commande_id );
if ( $tentatives >= 5 ) {
error_log( "Abandon de la synchronisation pour la commande {$commande_id} après 5 tentatives." );
return;
}
try {
appeler_api_erp( $commande_id );
delete_transient( 'tentatives_sync_' . $commande_id );
} catch ( Exception $e ) {
set_transient( 'tentatives_sync_' . $commande_id, $tentatives + 1, DAY_IN_SECONDS );
as_schedule_single_action( time() + 300, 'synchroniser_commande_erp', [ $commande_id ] );
throw $e; // L'échec reste visible dans l'écran Action Scheduler
}
} );
Laisser l’exception remonter, via un nouveau throw, permet à Action Scheduler de marquer correctement l’action comme échouée dans son propre écran de suivi, plutôt que de l’afficher comme « terminée » alors qu’elle a en réalité échoué silencieusement.
Nettoyer la file existante avant de déployer le correctif
Le correctif seul ne suffit pas si des milliers d’occurrences sont déjà en attente. WP-CLI permet de purger les actions en double pour un hook donné avant de redéployer :
wp action-scheduler action list --hook=synchroniser_commande_erp --status=pending --format=ids | \
xargs -n1 wp action-scheduler action delete
Prévention : un compteur de tentatives dès la conception
Toute tâche planifiée susceptible d’échouer à cause d’une dépendance externe — API tierce, service distant, ressource temporairement indisponible — devrait intégrer, dès sa conception, une limite explicite de tentatives et un mécanisme d’alerte au-delà de ce seuil. Un simple appel à as_has_scheduled_action() avant de reprogrammer permet également d’éviter qu’une même tâche ne soit dupliquée par un autre déclencheur pendant qu’une tentative est déjà en cours.
Le repère qu’on applique désormais systématiquement : aucun bloc
finallyne doit reprogrammer une tâche sans avoir d’abord consulté un compteur de tentatives borné. Sans cette limite, un échec temporaire se transforme en incident permanent.
En résumé
Une tâche qui se replanifie en boucle après un échec silencieux part presque toujours d’un même schéma : une exception interceptée mais jamais relancée, combinée à une reprogrammation inconditionnelle. Le correctif ne demande pas de bibliothèque supplémentaire, seulement de laisser Action Scheduler faire son travail de suivi des échecs, et de borner explicitement les tentatives plutôt que de les laisser s’accumuler sans limite.