« Fatal error: Allowed memory size of 268435456 bytes exhausted » : ce message, apparu du jour au lendemain dans le journal d’erreurs, concerne une tâche planifiée qui synchronise chaque nuit le contenu d’une extension vers un service externe. Aucune modification récente du code n’explique, a priori, cette apparition soudaine, ce qui rend le diagnostic initial trompeur.
Symptôme
Le journal PHP affiche l’erreur systématiquement au même point du traitement, sans jamais dépasser la limite fixée à 256 mégaoctets dans wp-config.php les nuits précédentes. La tâche fonctionnait sans incident depuis plusieurs mois, ce qui exclut a priori un défaut de conception initial évident.
Diagnostic

Le code de la tâche planifiée révèle rapidement l’origine du problème : un appel unique à get_posts() avec numberposts fixé à -1, censé charger l’ensemble du contenu concerné en une seule fois :
add_action( 'extension_synchronisation_nocturne', function() {
$articles = get_posts( array(
'post_type' => 'produit',
'numberposts' => -1,
'post_status' => 'publish',
) );
foreach ( $articles as $article ) {
extension_envoyer_vers_service_externe( $article );
}
} );
Ce code fonctionnait sans souci quand le catalogue comptait quelques centaines de produits. Mais chaque objet WP_Post chargé en mémoire, avec l’intégralité de son contenu et de ses métadonnées, occupe un espace non négligeable : la croissance progressive et invisible du catalogue, ajoutée semaine après semaine, a fini par dépasser la limite de mémoire allouée, sans qu’aucune modification de code ne soit en cause.
Correctif
La solution ne consiste pas à augmenter la limite de mémoire PHP, ce qui ne ferait que repousser le problème à un volume de contenu légèrement supérieur. Le traitement doit être découpé en lots de taille fixe, avec un décalage (offset) progressif, et les objets traités doivent être libérés explicitement de la mémoire au fur et à mesure :
add_action( 'extension_synchronisation_nocturne', function() {
$taille_lot = 200;
$offset = 0;
do {
$articles = get_posts( array(
'post_type' => 'produit',
'numberposts' => $taille_lot,
'offset' => $offset,
'post_status' => 'publish',
'fields' => 'ids',
) );
foreach ( $articles as $id ) {
extension_envoyer_vers_service_externe( get_post( $id ) );
clean_post_cache( $id );
}
$offset += $taille_lot;
} while ( count( $articles ) === $taille_lot );
} );
Le paramètre fields => 'ids' évite de charger d’emblée l’objet complet pour chaque article, et l’appel à clean_post_cache() après traitement libère la copie mise en cache de l’objet, ce qui empêche l’accumulation silencieuse d’objets en mémoire pendant l’exécution de la boucle.
Une variante avec Action Scheduler
Pour un volume encore plus important, remplacer la boucle unique par plusieurs tâches enchaînées via Action Scheduler évite également le risque de dépassement du temps d’exécution maximal, souvent atteint en même temps que la limite de mémoire sur les gros catalogues : chaque lot devient sa propre tâche planifiée, avec sa propre allocation mémoire repartant de zéro.
Prévention
- Ne jamais utiliser
numberposts => -1dans une tâche planifiée destinée à durer : ce paramètre convient à un affichage ponctuel sur un volume connu et borné, pas à un traitement récurrent sur un contenu en croissance. - Surveiller la mémoire réellement consommée par une tâche planifiée avec
memory_get_peak_usage(), journalisée à la fin de chaque exécution, pour détecter une dérive progressive avant qu’elle ne provoque une erreur fatale en production. - Systématiser le traitement par lots dès la conception d’une tâche planifiée qui interroge du contenu, même si le volume initial semble anodin : un volume qui double chaque année transforme un code fonctionnel en incident différé, sans qu’aucune ligne n’ait été modifiée entre-temps.
Pour aller plus loin
Ce type d’incident illustre un piège classique des tâches planifiées : un code correct au moment de son écriture peut devenir défaillant uniquement parce que le volume de données qu’il traite a grossi, sans qu’aucune régression de code ne soit responsable. Concevoir dès le départ un traitement par lots, même pour un volume initial modeste, évite d’avoir à réagir en urgence le jour où la limite de mémoire est enfin atteinte.