Un client vend des places de concert sur son site. Toutes les nuits à 3 h, un import synchronise les stocks et les descriptions depuis un flux XML fourni par le partenaire billetterie. Un matin, la chargée de communication modifie à la main la description d’un événement à 3 h 02, pendant que l’import tourne encore. À 9 h, elle rouvre la page : sa modification a disparu, remplacée par une version datant de la veille.
Ce n’est pas un bug de cache, ni un souci de synchronisation entre serveurs. C’est une course entre deux appels à wp_insert_post() qui touchent le même article au même moment, et WordPress gère très mal ce scénario par défaut.
Le symptôme
Le contenu affiché correspond à une version antérieure à la dernière modification connue. Dans l’écran des révisions, on retrouve bien la bonne version, mais elle n’est plus celle qui est publiée. Aucune erreur PHP, aucun log d’échec : tout s’est exécuté « normalement », c’est justement le problème.
Le pattern se reproduit uniquement quand deux conditions sont réunies : un traitement automatisé (import, synchronisation, tâche cron) qui appelle wp_insert_post() ou wp_update_post() sur un article, et une intervention humaine sur ce même article dans la même fenêtre de quelques secondes.
Le diagnostic

La cause tient à l’ordre d’écriture en base. Chaque appel à wp_insert_post() lit d’abord la ligne existante dans wp_posts, construit un tableau de données, éventuellement crée une révision de l’état courant via wp_save_post_revision(), puis écrit la nouvelle version. Si le processus A (l’import) lit l’article avant que le processus B (la sauvegarde admin) n’ait écrit sa modification, l’import travaille sur une image périmée de l’article. Quand il écrit à son tour, il réécrit cette image périmée, révision comprise, par-dessus la modification de B.
Le point qui trompe la plupart des développeurs : on regarde l’historique des révisions et on suppose que la plus récente en date est la bonne. Ce n’est pas vrai ici, parce que la révision créée par l’import porte un timestamp de création postérieur à celui de la modification manuelle, alors que son contenu est antérieur. On peut le vérifier en comparant le contenu de chaque révision via l’écran revision.php, ou en interrogeant directement la table :
SELECT ID, post_modified, post_content
FROM wp_posts
WHERE post_parent = 4821
AND post_type = 'revision'
ORDER BY post_modified DESC
LIMIT 5;
On y voit clairement deux révisions très proches dans le temps mais avec un contenu différent, l’une portant la modification humaine, l’autre non.
Le correctif
La solution la plus robuste consiste à ne jamais laisser l’import réécrire un article sans vérifier qu’il travaille sur la dernière version connue. On ajoute un verrou logique avant le traitement, sur le modèle de ce que fait déjà WordPress pour l’édition concurrente dans l’admin (wp_check_post_lock()), mais appliqué cette fois au script d’import :
function import_verrouiller_post( int $post_id ): bool {
$verrou = get_post_meta( $post_id, '_import_lock', true );
if ( $verrou && ( time() - (int) $verrou ) < 60 ) {
return false; // un autre traitement est en cours
}
update_post_meta( $post_id, '_import_lock', time() );
return true;
}
Autre option, plus fine : comparer le post_modified_gmt lu au début du script avec celui présent en base juste avant l'écriture finale, et abandonner l'écriture si les deux ne correspondent plus. C'est le même principe que la gestion des conflits optimistes qu'on trouve dans beaucoup d'API REST, transposé à l'API Post.
Réduire la fenêtre de risque
- Planifier les imports lourds en dehors des heures de travail de l'équipe éditoriale.
- Découper l'import en lots plus courts pour réduire le temps pendant lequel un article reste « en cours de traitement ».
- Désactiver la création de révision pendant un import purement technique, avec le filtre
wp_revisions_to_keepramené à 0 pour ce contexte précis, si l'historique n'a pas de valeur métier pour ces écritures.
La prévention
Sur ce projet, la correction retenue a combiné deux mesures : un verrou par transient avec une durée de vie de deux minutes autour de chaque écriture d'import, et une alerte Slack automatique quand le script détecte qu'un article a été modifié entre sa lecture et son écriture. En six mois d'exploitation, l'alerte s'est déclenchée quatre fois, et à chaque fois elle a évité une perte de contenu silencieuse.
Un import qui écrit sans lire l'état courant juste avant d'écrire n'est pas un import fiable, c'est un pari sur le silence des utilisateurs.
Il faut aussi former l'équipe éditoriale : si un import tourne à heure fixe, la documentation interne doit le mentionner explicitement, pour que personne ne modifie un article dans cette fenêtre sans le savoir.
Prévention à long terme
Pour les architectures plus exigeantes, on peut aller plus loin en confiant les écritures automatisées à une file d'attente traitée séquentiellement (Action Scheduler, par exemple) plutôt qu'à un cron qui frappe directement la base. Cela élimine une bonne partie des scénarios de concurrence, puisque les tâches s'exécutent une par une plutôt qu'en parallèle avec les actions humaines.
En résumé
Une révision plus récente en date n'est pas forcément plus récente en contenu : c'est le piège central de ce bug. Dès qu'un script automatisé et un humain peuvent toucher le même article, il faut un mécanisme de verrou ou de détection de conflit, sans quoi WordPress appliquera loyalement la dernière écriture reçue, même si elle efface un travail plus récent.