Redis caching com Claude Code: TTL, invalidação e anti-stampede
Guia prático de Redis caching com Claude Code: desenho de keys, TTL, invalidação, Node.js, testes e checklist de revisão.
Redis é um armazenamento chave-valor rápido em memória: ele mantém dados na RAM e recupera cada valor por uma key. É ótimo para agregados de banco, respostas de APIs externas, rankings, sessões curtas e rate limits. Mas, sem política clara, pedir ao Claude Code para “adicionar Redis” pode gerar preço antigo, estoque desatualizado, dados pessoais em cache compartilhado ou scans perigosos em produção.
Este guia transforma Redis caching em um fluxo que o Claude Code consegue implementar e revisar: política, key design, TTL, invalidação, prevenção de cache stampede, Node.js, testes e checklist. Para a visão de múltiplas camadas, veja Claude Code caching strategies. Se Redis também sustenta filas, leia 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
Defina a política primeiro
Antes do código, coloque a política em CLAUDE.md ou no prompt. Claude Code lê o fluxo técnico, mas não sabe sozinho por quanto tempo um dado pode ficar antigo sem dano ao negócio.
| Dado | Key exemplo | TTL | Invalidação | Risco |
|---|---|---|---|---|
| Lista pública de produtos | claudecodelab:v1:products:list:pt | 5 min | Apagar após update do produto | Não deixar preço só no TTL |
| Artigo | claudecodelab:v1:posts:item:{slug} | 10 min | Publicar, editar ou remover | Não cachear rascunhos |
| Métricas admin | claudecodelab:v1:analytics:daily:{date} | 30 s | Normalmente TTL | Não usar como verdade contábil |
| API externa | claudecodelab:v1:exchange-rate:usd-brl | 1 a 15 min | TTL ou refresh manual | Conferir termos do provedor |
| Dados de usuário logado | Excluir por padrão | 0 s | Nenhuma | Evitar cache compartilhado |
Dados públicos, repetíveis e regeneráveis são bons candidatos. Permissões, cobrança, autenticação e dados pessoais ficam fora, salvo se houver desenho de segurança próprio.
Prompt para Claude Code
Adicione uma camada Redis cache-aside a esta aplicação Node.js.
Requisitos:
1. Usar o pacote oficial node-redis chamado redis
2. Construir keys como claudecodelab:v1:{domain}:{resource}:{id}
3. Escolher TTLs pela política de cache e adicionar jitter de até 10%
4. Em updates, apagar keys relacionadas somente depois do DB write ter sucesso
5. Não usar KEYS em produção; usar keys conhecidas, SCAN ou sets de keys relacionadas
6. Evitar cache stampede com um lock curto para keys populares
7. Adicionar testes node:test para key, faixa de TTL, getOrSet e misses concorrentes
Retorne:
- Arquivos alterados
- Testes executados
- Dados excluídos do cache e o motivo
Use isso junto com a checklist de code review do Claude Code para revisar invalidação, não apenas latência.
Implementação Node.js
O cliente oficial para Node.js é o pacote redis; a conexão está documentada no 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
Centralize key e TTL.
// 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 };
Crie uma conexão Redis compartilhada.
// 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 };
O helper usa cache-aside com JSON, TTL e lock curto SET NX PX.
// 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 };
Exemplo executável. Em produção, troque loadProductsFromDb() por Prisma, Supabase ou API. Veja também Prisma ORM with Claude Code e Supabase integration with 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: "pt", name: "Template CLAUDE.md", price: 9, published: true },
{ id: "p2", locale: "pt", name: "Treinamento 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, "pt");
const second = await listPublishedProducts(cache, "pt");
console.log({ firstStatus: first.cacheStatus, secondStatus: second.cacheStatus, products: second.value });
await cache.invalidate([cacheKey(["products", "list", "pt"])]);
await closeRedis();
}
main().catch(async (error) => {
console.error(error);
await closeRedis();
process.exitCode = 1;
});
node demo-products.js
Testes
Teste key, TTL, hit e miss concorrente.
// 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", "pt/BR"]), "claudecodelab:v1:products:list:pt%2Fbr");
});
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
Casos de uso
Catálogos públicos e listas de artigos são o primeiro caso: muita gente recebe a mesma resposta e a invalidação é simples.
Dashboards admin são o segundo caso. PV, conversão e receita preliminar podem ficar alguns segundos ou minutos atrasados; permissão, boleto, fatura e segurança não.
APIs externas são o terceiro caso. Câmbio, clima, planos SaaS e metadados públicos podem ser cacheados brevemente se os termos permitirem.
Rate limits são o quarto caso. INCR e EXPIRE funcionam bem para janelas curtas; autenticação pede revisão de segurança separada.
Armadilhas
Key incompleta reaproveita resposta errada. Se idioma, moeda, tenant, role, query ou versão mudam o resultado, entram na key.
Não apague cache antes do write no DB. A ordem segura é write bem-sucedido, apagar keys Redis conhecidas e depois purgar CDN se necessário.
Evite KEYS user:* em produção. Use keys conhecidas, sets de keys relacionadas ou comandos de manutenção com SCAN.
Para cache negativo, salve { found: false }; o helper acima trata null como miss.
Checklist de revisão
- Dados pessoais, permissões e cobrança excluídos ou justificados
- Keys completas para locale, tenant, role, query e version
- TTL explicado por frescor de negócio
- Invalidação após write bem-sucedido
- Sem
KEYSem produção - Lock, jitter ou fallback contra stampede
- Testes de key, TTL, hit e miss concorrente
- Métricas ou logs de hit rate
Referências e próximo passo
Inclua nas tarefas do Claude Code a Redis documentation, node-redis guide, Redis patterns e Redis optimization.
Para transformar isso em workflow de equipe, comece pelos produtos e templates ClaudeCodeLab. Para mapear Redis, CDN, writes de DB e observabilidade em um repositório real, use treinamento e consultoria Claude Code.
No demo Node.js de Masa, a primeira chamada executou o loader, a segunda virou Redis hit e dois misses concorrentes rodaram o loader apenas uma vez. O ganho principal veio de escrever key, TTL, invalidação e critérios de review antes de pedir mudanças ao Claude Code.
PDF grátis: cheatsheet do Claude Code
Informe seu e-mail e baixe uma página com comandos, hábitos de revisão e workflows seguros.
Cuidamos dos seus dados e não enviamos spam.
Sobre o autor
Masa
Engenheiro focado em workflows práticos com Claude Code.
Artigos relacionados
Permission receipt no Claude Code: escopo, prova e rollback
Padrão de permission receipt para Claude Code: ações permitidas, limites de aprovação, comandos de prova, rollback e CTA de receita.
Agent Harness seguro para Claude Code e Codex: permissoes, verificacao e rollback
Monte uma base segura para agentes com Claude Code e Codex usando politicas, plano, verificacao e recuperacao.
Subagentes no Claude Code: guia prático para delegar trabalho com segurança
Guia prático de subagentes no Claude Code para dividir artigos e código: regras, prompts, riscos e checklist.