wp cache flush sur un site de 300 000 articles : cette commande, exécutée par réflexe après chaque publication, a mis un serveur à genoux pendant plus d’un an avant qu’on ne comprenne pourquoi. Le thème bloc livré à ce média régional générait ses pages d’archive à partir de patterns partagés entre des dizaines de catégories, et chaque purge globale forçait la reconstruction simultanée de milliers de fragments de cache.
Ce retour d’expérience décrit la stratégie de purge ciblée par catégorie mise en place, ainsi que l’optimisation du rendu des patterns partagés qui a suivi. La question du CDN d’images et celle de la monétisation publicitaire ne sont pas traitées ici : ce sont des chantiers séparés, menés par d’autres prestataires sur ce projet.
Le problème : une purge qui coûte plus cher que le gain
Le thème bloc utilisait un unique pattern archive-header partagé par les 40 catégories du site, incluant un bloc de requête affichant les articles les plus lus de la rubrique. Chaque nouvel article déclenchait, via un hook mal calibré sur save_post, un appel à wp_cache_flush() qui vidait l’intégralité de l’object cache Redis, y compris les objets sans rapport avec la publication en cours.
Sur 300 000 articles et un trafic soutenu, cette purge globale provoquait un pic de charge à chaque publication, aux heures où la rédaction publie le plus : entre 7 h et 9 h. Le serveur régénérait alors, en quelques secondes, l’ensemble des pages d’archive du site simultanément sollicitées par les visiteurs.
La solution : purger par groupe de cache
La première étape a consisté à remplacer le flush global par des groupes de cache dédiés, un par catégorie, en s’appuyant sur wp_cache_add_non_persistent_groups() et une clé de cache composée de l’identifiant de catégorie et d’un horodatage de dernière modification stocké en option.

function wpm_bump_category_cache_version( $post_id ) {
$categories = get_the_category( $post_id );
foreach ( $categories as $category ) {
$key = 'wpm_cat_cache_version_' . $category->term_id;
update_option( $key, time(), false );
}
}
add_action( 'save_post_post', 'wpm_bump_category_cache_version' );
function wpm_get_category_cache_key( $term_id ) {
$version = get_option( 'wpm_cat_cache_version_' . $term_id, '0' );
return 'archive_header_' . $term_id . '_' . $version;
}
Cette clé versionnée est ensuite utilisée pour envelopper le rendu du pattern archive-header dans un appel à wp_cache_get() / wp_cache_set(), avec une expiration longue puisque l’invalidation se fait désormais par changement de version, non par durée.
Optimiser le rendu du pattern lui-même
La purge ciblée a réglé le pic de charge lié aux publications, mais le temps de génération d’une page d’archive restait élevé : le bloc de requête « articles les plus lus » exécutait une requête WP_Query triée par un champ de compteur de vues à chaque affichage, y compris en cas de cache manquant sur une seule catégorie parmi quarante.
- Le compteur de vues, mis à jour à chaque visite, a été déplacé vers une agrégation asynchrone via une tâche
wp_cronhoraire plutôt qu’unUPDATEsynchrone sur chaque page vue. - La requête de tri a été indexée en base via
meta_queryavec une clé numérique dédiée, évitant un tri en mémoire côté MySQL sur des dizaines de milliers de lignes. - Le rendu final du pattern a été mis en cache de fragment séparément de la requête, avec sa propre clé de version, pour permettre de rafraîchir les vues sans invalider tout l’en-tête d’archive.
Les chiffres après trois mois
| Indicateur | Avant | Après |
|---|---|---|
| Temps de rendu moyen d’une archive | 1,8 s | 0,3 s |
| Purges de cache par jour | ~450 | ~40 |
| Pic CPU aux heures de publication | 92 % | 34 % |
Le gain le plus visible pour la rédaction : la publication d’un article n’entraîne plus de ralentissement perceptible sur le reste du site, ce qui a permis de supprimer une contrainte opérationnelle absurde consistant à éviter de publier plusieurs articles à la même minute.
Un piège évité de justesse
Le versionnement par option a failli créer un nouveau problème : sans limite, la table wp_options aurait accumulé une entrée orpheline par catégorie supprimée. Un nettoyage sur le hook delete_term a été ajouté pour éviter cette dérive, un détail facile à négliger dans ce genre de dispositif.
Notre verdict
La leçon principale de ce projet tient en une phrase : sur un site à fort volume, la granularité de l’invalidation de cache compte davantage que sa durée. Un cache généreux mais purgé finement bat systématiquement un cache court mais purgé globalement. La contrepartie est une complexité de code plus élevée, qu’il faut documenter soigneusement pour l’équipe de rédaction technique qui reprendra ce thème bloc dans quelques années.