Advanced (Mis à jour: 01/06/2026)

Cache Redis avec Claude Code : TTL, invalidation et anti-stampede

Guide pratique pour concevoir un cache Redis avec Claude Code : keys, TTL, invalidation, code Node.js, tests et checklist de review.

Cache Redis avec Claude Code : TTL, invalidation et anti-stampede

Redis est un magasin clé-valeur rapide en mémoire : les données sont gardées en RAM et retrouvées par une clé. C’est excellent pour des agrégats de base de données, des réponses d’API externes, des classements, des sessions courtes et des compteurs de rate limit. Mais un cache mal cadré peut servir un ancien prix, masquer une mise à jour ou exposer des données personnelles.

Ce guide donne une méthode utilisable avec Claude Code : politique de cache, conception des keys, TTL, invalidation, prévention du cache stampede, implémentation Node.js, tests et checklist de review. Pour le contexte multi-couches, lisez aussi Claude Code caching strategies. Si Redis sert aussi aux jobs, voyez Claude Code queue system.

flowchart LR
  Request["HTTP request"] --> Cache["Redis cache"]
  Cache -->|hit| Response["Fast response"]
  Cache -->|miss| Lock["Short lock"]
  Lock --> Loader["DB or external API"]
  Loader --> Cache
  Loader --> Response
  Admin["Update event"] --> Invalidate["Delete known keys"]
  Invalidate --> Cache

Commencer par la politique

Avant de laisser Claude Code coder, écrivez la politique dans CLAUDE.md ou dans la tâche. Le code montre d’où vient la donnée, pas combien de temps elle peut être périmée sans risque métier.

DonnéeExemple de keyTTL indicatifInvalidationRisque
Liste produits publiqueclaudecodelab:v1:products:list:fr5 minutesSupprimer après update produitNe pas laisser les prix au TTL seul
Articleclaudecodelab:v1:posts:item:{slug}10 minutesPublier, modifier, dépublierNe pas cacher brouillons et previews
Statistiques adminclaudecodelab:v1:analytics:daily:{date}30 secondesSouvent TTL seulPas pour la vérité comptable
API externeclaudecodelab:v1:exchange-rate:usd-eur1 à 15 minutesTTL ou refresh manuelVérifier les conditions fournisseur
Données utilisateur connectéPar défaut exclues0 secondeAucuneÉviter le cache partagé

La règle pratique : données publiques, répétables et régénérables peuvent aller dans Redis ; permissions, facturation, authentification et données personnelles restent hors cache sauf design de sécurité explicite.

Prompt pour Claude Code

Ajoute une couche Redis cache-aside à cette application Node.js.

Exigences :
1. Utiliser le package officiel node-redis nommé redis
2. Construire les keys sous la forme claudecodelab:v1:{domain}:{resource}:{id}
3. Choisir les TTL depuis la politique de cache et ajouter jusqu'à 10% de jitter
4. Après une mise à jour, supprimer les keys connues seulement après succès du DB write
5. Ne pas utiliser KEYS en production ; utiliser keys connues, SCAN ou sets de keys liées
6. Ajouter un lock court contre le cache stampede des keys populaires
7. Ajouter des tests node:test pour key, TTL, getOrSet et miss concurrents

Retour :
- Fichiers modifiés
- Tests exécutés
- Données exclues du cache et raison

Ajoutez ces attentes à votre checklist de review Claude Code pour vérifier l’invalidation, pas seulement la vitesse.

Implémentation Node.js

Le client officiel est le package redis; le flux de connexion est documenté dans node-redis guide.

mkdir redis-cache-demo
cd redis-cache-demo
npm init -y
npm install redis
docker run --name redis-cache-demo -p 6379:6379 -d redis:7-alpine

Mettez les règles de key et TTL dans un fichier dédié.

// cache-policy.js
const CACHE_PREFIX = "claudecodelab";
const CACHE_VERSION = "v1";

const CACHE_POLICY = {
  productList: { ttl: 300, jitter: 30 },
  productItem: { ttl: 600, jitter: 60 },
  dailyStats: { ttl: 30, jitter: 5 },
};

function normalizePart(value) {
  const part = String(value).trim().toLowerCase();
  if (part.length === 0) {
    throw new Error("cache key part must not be empty");
  }
  return encodeURIComponent(part);
}

function cacheKey(parts) {
  if (!Array.isArray(parts) || parts.length === 0) {
    throw new Error("cacheKey requires a non-empty parts array");
  }
  return [CACHE_PREFIX, CACHE_VERSION, ...parts.map(normalizePart)].join(":");
}

function ttlWithJitter(baseSeconds, maxJitterSeconds = 30) {
  if (!Number.isInteger(baseSeconds) || baseSeconds <= 0) {
    throw new Error("base TTL must be a positive integer");
  }
  const jitter = Math.max(0, Math.floor(maxJitterSeconds));
  return baseSeconds + Math.floor(Math.random() * (jitter + 1));
}

module.exports = { CACHE_POLICY, cacheKey, ttlWithJitter };

Partagez une connexion Redis au lieu de reconnecter à chaque requête.

// redis-client.js
const { createClient } = require("redis");

const redis = createClient({
  url: process.env.REDIS_URL || "redis://localhost:6379",
});

redis.on("error", (error) => {
  console.error("Redis Client Error", error);
});

let connecting;

async function getRedis() {
  if (redis.isOpen) return redis;
  if (!connecting) {
    connecting = redis.connect();
  }
  await connecting;
  return redis;
}

async function closeRedis() {
  if (redis.isOpen) {
    await redis.quit();
  }
  connecting = undefined;
}

module.exports = { getRedis, closeRedis };

Le helper lit Redis, appelle le loader seulement en cas de miss et utilise un court lock SET NX PX pour éviter qu’une key populaire expirée frappe la base en masse.

// redis-cache.js
const { randomUUID } = require("node:crypto");

const UNLOCK_SCRIPT = `
if redis.call("get", KEYS[1]) == ARGV[1] then
  return redis.call("del", KEYS[1])
end
return 0
`;

function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

class RedisJsonCache {
  constructor(redis, options = {}) {
    this.redis = redis;
    this.defaultTtl = options.defaultTtl || 300;
    this.lockMs = options.lockMs || 5000;
    this.waitMs = options.waitMs || 50;
    this.waitRetries = options.waitRetries || 10;
  }

  async get(key) {
    const raw = await this.redis.get(key);
    if (raw === null) return null;
    try {
      return JSON.parse(raw);
    } catch {
      await this.redis.del(key);
      return null;
    }
  }

  async set(key, value, ttlSeconds = this.defaultTtl) {
    if (!Number.isInteger(ttlSeconds) || ttlSeconds <= 0) {
      throw new Error("ttlSeconds must be a positive integer");
    }
    await this.redis.set(key, JSON.stringify(value), { EX: ttlSeconds });
  }

  async invalidate(keys) {
    const list = Array.isArray(keys) ? keys : [keys];
    if (list.length === 0) return 0;
    return this.redis.del(list);
  }

  async getOrSet(key, ttlSeconds, loader) {
    const cached = await this.get(key);
    if (cached !== null) {
      return { value: cached, cacheStatus: "hit" };
    }

    const lockKey = `${key}:lock`;
    const token = randomUUID();
    const acquired = await this.redis.set(lockKey, token, { NX: true, PX: this.lockMs });

    if (acquired === "OK") {
      try {
        const fresh = await loader();
        await this.set(key, fresh, ttlSeconds);
        return { value: fresh, cacheStatus: "miss" };
      } finally {
        await this.redis.eval(UNLOCK_SCRIPT, { keys: [lockKey], arguments: [token] });
      }
    }

    for (let attempt = 0; attempt < this.waitRetries; attempt += 1) {
      await sleep(this.waitMs);
      const afterWait = await this.get(key);
      if (afterWait !== null) {
        return { value: afterWait, cacheStatus: "hit-after-wait" };
      }
    }

    const fallback = await loader();
    await this.set(key, fallback, Math.max(5, Math.floor(ttlSeconds / 3)));
    return { value: fallback, cacheStatus: "miss-after-timeout" };
  }
}

module.exports = { RedisJsonCache };

Voici un exemple exécutable. Dans un vrai projet, remplacez loadProductsFromDb() par Prisma, Supabase ou votre client API. Voir aussi Prisma ORM avec Claude Code et Supabase avec Claude Code.

// demo-products.js
const { CACHE_POLICY, cacheKey, ttlWithJitter } = require("./cache-policy");
const { getRedis, closeRedis } = require("./redis-client");
const { RedisJsonCache } = require("./redis-cache");

const db = {
  products: [
    { id: "p1", locale: "fr", name: "Modèle CLAUDE.md", price: 9, published: true },
    { id: "p2", locale: "fr", name: "Formation Claude Code", price: 199, published: true },
  ],
};

async function loadProductsFromDb(locale) {
  await new Promise((resolve) => setTimeout(resolve, 80));
  return db.products.filter((product) => product.locale === locale && product.published);
}

async function listPublishedProducts(cache, locale) {
  const key = cacheKey(["products", "list", locale]);
  const ttl = ttlWithJitter(CACHE_POLICY.productList.ttl, CACHE_POLICY.productList.jitter);
  return cache.getOrSet(key, ttl, () => loadProductsFromDb(locale));
}

async function main() {
  const redis = await getRedis();
  const cache = new RedisJsonCache(redis);
  const first = await listPublishedProducts(cache, "fr");
  const second = await listPublishedProducts(cache, "fr");
  console.log({ firstStatus: first.cacheStatus, secondStatus: second.cacheStatus, products: second.value });
  await cache.invalidate([cacheKey(["products", "list", "fr"])]);
  await closeRedis();
}

main().catch(async (error) => {
  console.error(error);
  await closeRedis();
  process.exitCode = 1;
});
node demo-products.js

Tests

Testez la key, le TTL, le hit et les miss concurrents.

// redis-cache.test.js
const test = require("node:test");
const assert = require("node:assert/strict");
const { cacheKey, ttlWithJitter } = require("./cache-policy");
const { RedisJsonCache } = require("./redis-cache");

class FakeRedis {
  constructor() {
    this.store = new Map();
  }

  async get(key) {
    const entry = this.store.get(key);
    if (!entry) return null;
    if (entry.expiresAt && entry.expiresAt <= Date.now()) {
      this.store.delete(key);
      return null;
    }
    return entry.value;
  }

  async set(key, value, options = {}) {
    if (options.NX && (await this.get(key)) !== null) return null;
    const ttlMs = options.PX || (options.EX ? options.EX * 1000 : 0);
    this.store.set(key, { value, expiresAt: ttlMs ? Date.now() + ttlMs : 0 });
    return "OK";
  }

  async del(keys) {
    const list = Array.isArray(keys) ? keys : [keys];
    let deleted = 0;
    for (const key of list) {
      if (this.store.delete(key)) deleted += 1;
    }
    return deleted;
  }

  async eval(_script, options) {
    const [key] = options.keys;
    const [token] = options.arguments;
    if ((await this.get(key)) === token) return this.del(key);
    return 0;
  }
}

test("cacheKey encodes dynamic parts", () => {
  assert.equal(cacheKey(["Products", "List", "fr/FR"]), "claudecodelab:v1:products:list:fr%2Ffr");
});

test("ttlWithJitter stays inside the expected range", () => {
  for (let i = 0; i < 50; i += 1) {
    const ttl = ttlWithJitter(300, 30);
    assert.ok(ttl >= 300);
    assert.ok(ttl <= 330);
  }
});

test("getOrSet caches the first loader result", async () => {
  const cache = new RedisJsonCache(new FakeRedis());
  let loads = 0;
  const first = await cache.getOrSet("products:list", 60, async () => {
    loads += 1;
    return [{ id: "p1" }];
  });
  const second = await cache.getOrSet("products:list", 60, async () => {
    loads += 1;
    return [{ id: "p2" }];
  });
  assert.equal(first.cacheStatus, "miss");
  assert.equal(second.cacheStatus, "hit");
  assert.equal(loads, 1);
  assert.deepEqual(second.value, [{ id: "p1" }]);
});

test("getOrSet waits instead of running duplicate loaders", async () => {
  const cache = new RedisJsonCache(new FakeRedis(), { waitMs: 5, waitRetries: 20 });
  let loads = 0;
  const loader = async () => {
    loads += 1;
    await new Promise((resolve) => setTimeout(resolve, 20));
    return { total: 42 };
  };
  const results = await Promise.all([
    cache.getOrSet("analytics:daily", 30, loader),
    cache.getOrSet("analytics:daily", 30, loader),
  ]);
  assert.equal(loads, 1);
  assert.deepEqual(results[0].value, { total: 42 });
  assert.deepEqual(results[1].value, { total: 42 });
});
node --test redis-cache.test.js

Cas d’usage

Les catalogues publics et listes d’articles sont le meilleur premier cas : beaucoup de lecteurs voient la même réponse, et les keys sont faciles à supprimer après édition.

Les dashboards admin sont un deuxième cas. PV, conversions et aperçu de revenu peuvent être vieux de quelques secondes ou minutes ; permissions, factures et paiement doivent rester hors cache.

Les API externes sont un troisième cas. Taux de change, météo, plans SaaS ou métadonnées publiques peuvent être gardés brièvement pour réduire la pression de rate limit.

Les rate limits sont un quatrième cas. INCR plus EXPIRE est adapté aux fenêtres courtes, mais l’authentification demande une revue de sécurité séparée.

Pièges

Une key incomplète est le bug le plus fréquent. Langue, devise, tenant, rôle, query et version doivent être dans la key s’ils changent le résultat.

Ne supprimez pas le cache avant le write DB. L’ordre sûr est : write réussi, suppression des keys Redis connues, purge CDN si nécessaire.

Évitez KEYS user:* en production. Préférez des keys connues, un Set de keys liées ou une commande de maintenance basée sur SCAN.

Si vous devez cacher un résultat négatif, stockez { found: false }; l’implémentation ci-dessus traite null comme un miss.

Checklist de review

  • Données personnelles, permissions et facturation exclues ou justifiées
  • Keys complètes : locale, tenant, rôle, query, version
  • TTL expliqué par un besoin métier
  • Invalidation après succès du write
  • Pas de KEYS en production
  • Lock, jitter ou fallback contre le stampede
  • Tests pour key, TTL, hit et miss concurrent
  • Logs ou métriques de hit rate disponibles

Références et suite

Ajoutez dans les tâches Claude Code les liens officiels : Redis documentation, node-redis guide, Redis patterns et Redis optimization.

Pour transformer cette approche en workflow d’équipe, commencez par les produits et templates ClaudeCodeLab. Pour relier Redis, CDN, writes DB et observabilité dans un vrai dépôt, utilisez Claude Code formation et consultation.

Dans le test Node.js de Masa, la première requête appelait le loader, la seconde devenait un hit Redis, et deux miss concurrents ne lançaient le loader qu’une fois. Le vrai gain vient de la politique : key, TTL, invalidation et review écrits avant de demander à Claude Code de modifier le code.

#Claude Code #Redis #cache #Node.js #performance
Gratuit

PDF gratuit: cheatsheet Claude Code

Saisissez votre email et téléchargez une page avec commandes, habitudes de review et workflow sûr.

Nous protégeons vos données et n'envoyons pas de spam.

Masa

À propos de l'auteur

Masa

Ingénieur spécialisé dans les workflows pratiques avec Claude Code.