« La base de données ne répond plus » : ce message d’alerte, reçu un matin, aurait dû concerner un seul site du réseau. Il en a concerné quarante-trois, hébergés sur la même instance MySQL mutualisée au sein d’une infrastructure de cinq cents sites WordPress destinée à des communes et intercommunalités. L’incident, provoqué par une requête mal indexée sur l’un des sites, a saturé les connexions disponibles pour l’ensemble du groupe hébergé sur la même instance.
Cet incident a servi de révélateur à plusieurs choix d’architecture pris progressivement, site après site, sans jamais être remis en question globalement à mesure que le réseau grossissait. Voici les antipatterns qu’il a mis en lumière, et ce que nous avons changé depuis pour ce client.
Ce qu’on observait : une isolation pensée pour la charge, pas pour la panne
L’infrastructure répartissait les cinq cents sites sur un ensemble d’instances de base de données par lots de cinquante, en fonction de la charge moyenne mesurée à un instant donné. Cette répartition optimisait bien l’usage des ressources en conditions normales, mais elle ne prenait pas en compte l’isolation en cas de défaillance : un site qui consomme toutes les connexions disponibles de son instance affecte mécaniquement les quarante-neuf autres partageant la même instance, quelle que soit leur propre charge.
Pourquoi c’est un problème : la répartition par charge crée une dépendance cachée entre des sites qui n’ont aucun lien fonctionnel entre eux — une commune de deux mille habitants et une intercommunalité de cent mille habitants peuvent se retrouver sur la même instance simplement parce que leurs profils de charge coïncidaient au moment du provisionnement.
Quoi faire : répartir par plafond de connexions garanti par site plutôt que par charge observée, avec une limite dure de connexions simultanées appliquée au niveau de chaque site via le pool de connexions applicatif, indépendamment de ce que font les sites voisins.
Ce qu’on observait : un secret de configuration partagé entre tous les sites
Le fichier de configuration de connexion à un service de cache Redis partagé utilisait les mêmes identifiants pour l’ensemble des cinq cents sites, générés une fois lors du provisionnement initial de l’infrastructure et jamais renouvelés depuis. Un accès compromis sur un seul site — par exemple via une extension vulnérable — donnait potentiellement accès en lecture aux données de cache de tous les autres.

Pourquoi c’est un problème : le cache d’objets peut contenir des données sensibles temporairement mises en cache (résultats de requêtes utilisateur, jetons de session), et un identifiant partagé transforme une compromission locale en risque global.
Quoi faire : un espace de noms Redis distinct par site avec un identifiant d’accès propre, généré automatiquement au provisionnement et stocké dans un gestionnaire de secrets dédié, jamais dans un fichier de configuration versionné ou partagé entre projets.
Ce qu’on observait : une supervision globale qui masquait les incidents locaux
Le tableau de bord de supervision affichait un taux de disponibilité agrégé sur l’ensemble du réseau. Un incident touchant quarante-trois sites sur cinq cents faisait à peine bouger ce pourcentage global, ce qui a retardé la détection réelle de l’ampleur de l’incident : l’alerte automatique s’est déclenchée près de vingt minutes après le début du problème, le seuil de déclenchement étant calibré sur le taux global et non sur un comptage de sites affectés.
Ce qui a changé
- Une alerte dédiée se déclenche désormais dès que plus de trois sites d’un même lot d’hébergement présentent une erreur simultanément, indépendamment du taux de disponibilité global.
- Chaque site dispose d’un contrôle de santé individuel, interrogé toutes les minutes, plutôt qu’un contrôle d’échantillon représentatif du réseau.
- Le tableau de bord distingue désormais la disponibilité par lot d’hébergement, ce qui permet d’identifier immédiatement quelle instance partagée est concernée.
Ce qu’on observait : aucun plafond de requêtes lentes par site
Aucun mécanisme ne limitait le temps d’exécution des requêtes SQL par site avant l’incident. Une requête sans index adapté, exécutée par un seul site lors d’un import de données, a pu tourner plusieurs minutes en consommant une connexion pendant tout ce temps, privant les autres sites de l’instance des connexions disponibles.
Quoi faire : un délai maximal d’exécution de requête configuré au niveau du pool de connexions, avec interruption automatique au-delà de dix secondes et alerte envoyée à l’équipe technique responsable du site concerné, avant que la connexion ne reste bloquée indéfiniment.
Sur une infrastructure mutualisée, l’optimisation des ressources et l’isolation des pannes tirent dans des directions opposées. Une architecture qui ne pense qu’à la première finit par payer la seconde au prix fort, généralement un matin où on ne s’y attend pas.
Notre retour d’expérience
Ces quatre antipatterns partagent un point commun : chacun paraissait raisonnable au moment où il a été introduit, dans un contexte où le réseau comptait bien moins de cinq cents sites. La leçon principale n’est pas de blâmer les choix initiaux, mais de revoir périodiquement l’architecture d’une infrastructure mutualisée à mesure qu’elle grossit, avant qu’un incident ne force cette révision dans l’urgence.