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.
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ée | Exemple de key | TTL indicatif | Invalidation | Risque |
|---|---|---|---|---|
| Liste produits publique | claudecodelab:v1:products:list:fr | 5 minutes | Supprimer après update produit | Ne pas laisser les prix au TTL seul |
| Article | claudecodelab:v1:posts:item:{slug} | 10 minutes | Publier, modifier, dépublier | Ne pas cacher brouillons et previews |
| Statistiques admin | claudecodelab:v1:analytics:daily:{date} | 30 secondes | Souvent TTL seul | Pas pour la vérité comptable |
| API externe | claudecodelab:v1:exchange-rate:usd-eur | 1 à 15 minutes | TTL ou refresh manuel | Vérifier les conditions fournisseur |
| Données utilisateur connecté | Par défaut exclues | 0 seconde | Aucune | É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
KEYSen 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.
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.
À propos de l'auteur
Masa
Ingénieur spécialisé dans les workflows pratiques avec Claude Code.
Articles liés
Permission receipt Claude Code : portée, preuves et rollback
Modèle de permission receipt pour Claude Code : actions autorisées, limites d'approbation, commandes de preuve, rollback et CTAs revenus.
Agent Harness securise pour Claude Code et Codex : permissions, verification et rollback
Construisez un Agent Harness pratique pour Claude Code et Codex avec politiques, plan, verification et recuperation.
Sous-agents Claude Code : guide pratique pour déléguer sans perdre le contrôle
Guide pratique des sous-agents Claude Code pour répartir articles et code : règles, prompts, pièges et checklist.