Une interface d'administration se met à renvoyer des erreurs 500. Aucun déploiement n'a eu lieu depuis plusieurs jours, le code n'a pas bougé, la base de données répond normalement. Le réflexe naturel — relire les derniers commits — ne mène nulle part, parce que la cause n'est pas dans l'application. Elle est deux couches en dessous.
Le symptôme trompeur
Le scénario est classique et déroutant : les pages publiques fonctionnent, mais toute page nécessitant une connexion renvoie une erreur serveur. Les journaux applicatifs affichent une exception liée à la lecture de session, parfois une erreur de connexion refusée, jamais un message qui mentionne explicitement la mémoire.
Cette dissociation entre le lieu où l'erreur s'affiche et le lieu où elle se produit est le cœur du problème de diagnostic. L'application signale honnêtement ce qu'elle constate : elle n'arrive pas à lire la session de l'utilisateur. Elle n'a aucun moyen de savoir pourquoi le magasin de sessions ne répond plus.
Ce qui se passe réellement
Redis conserve l'intégralité de ses données en mémoire vive. C'est ce qui le rend rapide, et c'est aussi sa contrainte structurante. Lorsqu'un processus Redis démarre sans limite de mémoire configurée, il n'a aucune raison de refuser une écriture : il alloue tant que le système lui en donne.
Dans un environnement conteneurisé, deux limites peuvent alors intervenir :
- La limite du conteneur. Si une contrainte mémoire est définie au niveau de l'orchestrateur, le noyau arrête le processus dès qu'elle est franchie. Le conteneur redémarre, recommence à se remplir, et se fait tuer à nouveau : c'est la boucle de redémarrage.
- La limite de la machine. En l'absence de contrainte par conteneur, c'est la mémoire de l'hôte qui s'épuise. Le mécanisme de dernier recours du noyau choisit alors un processus à sacrifier — souvent le plus gourmand, donc Redis, mais parfois un voisin innocent.
Il existe un troisième cas, plus subtil : Redis atteint une limite maxmemory correctement définie, mais avec une politique d'éviction réglée sur le refus. Le processus ne meurt pas ; il reste vivant et rejette toute écriture avec une erreur explicite indiquant que la commande n'est pas autorisée en situation de saturation. Les lectures continuent de fonctionner, ce qui rend le diagnostic encore plus déroutant : le service semble opérationnel.
Un conteneur qui redémarre en boucle est un conteneur qui, vu de l'extérieur, existe. Les outils de supervision qui se contentent de vérifier la présence d'un service ne détectent pas ce type de panne.
Pourquoi la panne devient totale
Un cache est par définition jetable. S'il disparaît, l'application doit simplement redevenir plus lente, le temps de recalculer ce qu'elle avait mémorisé. Un site correctement conçu survit à la perte de son cache.
La bascule d'un incident de performance vers une panne totale se produit au moment où la même instance Redis sert à autre chose que du cache. Or c'est la configuration par défaut de nombreux frameworks modernes : dès qu'un pilote Redis est disponible, il devient tentant de l'utiliser aussi pour les sessions, les files d'attente et les verrous distribués.
À partir de là, Redis n'est plus une optimisation : c'est une dépendance critique. Sa perte ne dégrade pas le service, elle l'interrompt. Et comme les sessions ne concernent que les utilisateurs connectés, la panne est partielle et donc difficile à repérer depuis l'extérieur — le site public reste vert dans les tableaux de bord.
Le correctif : trois niveaux
1. Imposer une limite et une politique d'éviction
C'est la correction fondamentale. Redis doit connaître sa limite et savoir quoi faire quand il l'atteint :
maxmemory 256mb
maxmemory-policy allkeys-lru
La politique allkeys-lru supprime les clés les moins récemment utilisées, quelle que soit leur nature. Elle convient à un usage strictement cache. Si la même instance héberge des sessions, préférez volatile-lru, qui ne supprime que les clés porteuses d'une date d'expiration — mais cette politique ne protège de rien si aucune clé n'expire : Redis se retrouve alors dans l'impossibilité de libérer de la place et recommence à refuser les écritures.
La limite doit rester nettement inférieure à la mémoire allouée au conteneur. Redis consomme au-delà des données elles-mêmes : tampons de réplication, files de sortie clients, fragmentation de l'allocateur. Une marge de 25 à 30 % est un point de départ raisonnable.
2. Séparer le jetable du critique
Redis expose plusieurs bases logiques numérotées. Les utiliser pour isoler les usages coûte une ligne de configuration et évite qu'une purge de cache n'emporte les sessions :
REDIS_CACHE_DB=1
REDIS_SESSION_DB=2
Cette séparation n'isole pas la mémoire — tout partage la même limite — mais elle rend les opérations de maintenance sûres. Vider le cache devient une action anodine au lieu d'un événement qui déconnecte tous les utilisateurs.
Pour un service véritablement critique, l'étape suivante consiste à faire tourner deux instances distinctes, avec des limites et des politiques propres. C'est la seule configuration où une saturation du cache ne peut structurellement pas affecter les sessions.
3. Dégrader proprement plutôt que tomber
Une application robuste ne devrait pas renvoyer une erreur 500 parce que son cache est indisponible. Envelopper les accès au cache de sorte qu'un échec retombe sur le calcul direct transforme une panne en simple ralentissement. Pour les sessions, la bascule est plus délicate — mais un magasin de secours vaut mieux qu'une page d'erreur.
Le piège du déploiement
Un détail opérationnel mérite une attention particulière, parce qu'il transforme un incident de quelques minutes en panne de plusieurs jours.
Beaucoup de scripts de déploiement ne reconstruisent que le service applicatif : ils récupèrent le code, rebâtissent l'image concernée et redémarrent ce conteneur précis. C'est efficace et rapide. Mais cela signifie que les services d'infrastructure — base de données, cache, file d'attente — ne sont jamais recréés par un déploiement.
Conséquence : un conteneur Redis entré en boucle de redémarrage y reste. Chaque déploiement passe à côté, chaque redémarrage applicatif retrouve le même cache cassé. L'équipe déploie, constate que l'erreur persiste, et cherche encore plus loin dans le code.
Deux mesures évitent ce piège :
- Une politique de redémarrage correcte sur les services d'infrastructure, pour qu'ils reviennent d'eux-mêmes après un arrêt inopiné.
- Une supervision qui teste la fonction, pas la présence. Vérifier qu'un conteneur existe ne dit rien de son état. Vérifier qu'une écriture puis une lecture aboutissent dit tout.
Vérifier en trois commandes
Face à des erreurs 500 inexpliquées sur les pages authentifiées, ces trois vérifications tranchent en moins d'une minute :
# 1. Le conteneur redémarre-t-il en boucle ?
docker ps --format '{{.Names}}\t{{.Status}}' | grep redis
# 2. Où en est la mémoire, et une limite est-elle définie ?
docker exec <conteneur> redis-cli INFO memory | grep -E 'used_memory_human|maxmemory_human|maxmemory_policy'
# 3. Le service accepte-t-il encore une écriture ?
docker exec <conteneur> redis-cli SET diagnostic ok EX 10
Un statut affichant des redémarrages répétés confirme la boucle de crash. Une limite à zéro confirme l'absence de garde-fou. Un refus d'écriture confirme la saturation. Dans les trois cas, la cause est identifiée et le code applicatif est hors de cause.
La leçon transposable
Cet incident illustre un principe qui dépasse largement Redis : un composant introduit comme optimisation devient une dépendance critique dès qu'on lui confie un état qui ne peut pas être reconstruit.
Le cache est un accélérateur ; les sessions sont une source de vérité. Les faire cohabiter sans limite, sans politique d'éviction et sans supervision fonctionnelle revient à faire reposer la disponibilité de tout un site sur un composant que personne ne considère comme critique — précisément parce qu'il a été installé pour aller plus vite.