Advanced (Actualizado: 1/6/2026)

Redis caching con Claude Code: TTL, invalidación y anti-stampede

Guía práctica para diseñar Redis caching con Claude Code: keys, TTL, invalidación, Node.js, pruebas y checklist de revisión.

Redis caching con Claude Code: TTL, invalidación y anti-stampede

Redis es un almacén clave-valor rápido en memoria: guarda datos en RAM y los recupera por key. Es útil para agregados de base de datos, respuestas de APIs externas, rankings, sesiones cortas y rate limits. También es fácil romper una aplicación con él si Claude Code no recibe una política clara: precios obsoletos, inventario incorrecto, datos de usuario en caché compartida o Redis bloqueado por búsquedas amplias.

Esta guía convierte Redis caching en un flujo de trabajo para Claude Code: política de caché, diseño de key, TTL, invalidación, prevención de cache stampede, implementación Node.js, pruebas y checklist de review. Para una vista de varias capas, lee Claude Code caching strategies. Si Redis también se usa para jobs, revisa 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

Política antes del código

Antes de implementar, escribe una tabla de política en CLAUDE.md o en el prompt. Claude Code puede leer el flujo de datos, pero no puede adivinar cuánto stale data tolera el negocio.

DatoKey de ejemploTTLInvalidaciónRiesgo
Lista pública de productosclaudecodelab:v1:products:list:es5 minBorrar tras actualizar productoNo dejar cambios de precio solo al TTL
Artículoclaudecodelab:v1:posts:item:{slug}10 minPublicar, editar o despublicarNo cachear drafts ni previews
Métricas adminclaudecodelab:v1:analytics:daily:{date}30 sNormalmente TTLNo usar como fuente contable
API externaclaudecodelab:v1:exchange-rate:usd-eur1 a 15 minTTL o refresh manualRevisar términos del proveedor
Datos de usuario logueadoExcluir por defecto0 sNingunaEvitar caché compartida

La regla: datos públicos, repetibles y regenerables son buenos candidatos; permisos, facturación, autenticación y datos personales quedan fuera salvo que haya un diseño de seguridad explícito.

Prompt para Claude Code

Añade una capa Redis cache-aside a esta app Node.js.

Requisitos:
1. Usar el paquete oficial node-redis llamado redis
2. Construir keys como claudecodelab:v1:{domain}:{resource}:{id}
3. Elegir TTL desde la política de caché y añadir jitter de hasta 10%
4. Tras updates, borrar keys relacionadas solo después de que el DB write tenga éxito
5. No usar KEYS en producción; usar keys conocidas, SCAN o sets de keys relacionadas
6. Evitar cache stampede con un lock corto para keys populares
7. Añadir pruebas node:test para key, rango TTL, getOrSet y misses concurrentes

Devuelve:
- Archivos modificados
- Pruebas ejecutadas
- Datos excluidos de caché y motivo

Incluye estas reglas en tu checklist de code review con Claude Code para revisar invalidación, no solo velocidad.

Implementación Node.js

El cliente oficial para Node.js es el paquete redis; el patrón está en la 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

Centraliza keys y 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 };

Usa un cliente Redis compartido.

// 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 };

El helper usa cache-aside con JSON, TTL y un lock corto 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 };

Ejemplo ejecutable. En una app real, cambia loadProductsFromDb() por Prisma, Supabase o tu API. Recursos relacionados: Prisma ORM with Claude Code y 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: "es", name: "Plantilla CLAUDE.md", price: 9, published: true },
    { id: "p2", locale: "es", name: "Formación 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, "es");
  const second = await listPublishedProducts(cache, "es");
  console.log({ firstStatus: first.cacheStatus, secondStatus: second.cacheStatus, products: second.value });
  await cache.invalidate([cacheKey(["products", "list", "es"])]);
  await closeRedis();
}

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

Pruebas

Prueba la key, el TTL, el hit y el miss concurrente.

// 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", "es/ES"]), "claudecodelab:v1:products:list:es%2Fes");
});

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 y listas de artículos son el primer caso: muchos lectores reciben la misma respuesta y las keys son fáciles de borrar tras una edición.

Dashboards admin son el segundo caso. PV, conversión e ingresos preliminares pueden estar unos segundos o minutos atrasados; permisos, facturas y seguridad no.

APIs externas son el tercer caso. Tipo de cambio, clima, planes SaaS y metadatos públicos pueden cachearse brevemente si los términos lo permiten.

Rate limits son el cuarto caso. INCR y EXPIRE funcionan bien para ventanas cortas; autenticación requiere revisión de seguridad separada.

Errores comunes

Una key incompleta reutiliza respuestas incorrectas. Si idioma, moneda, tenant, rol, query o versión cambian el resultado, deben estar en la key.

No borres la caché antes del write en DB. El orden seguro es write exitoso, borrar keys Redis conocidas y luego purgar CDN si aplica.

Evita KEYS user:* en producción. Usa keys conocidas, sets de keys relacionadas o comandos de mantenimiento con SCAN.

Si necesitas cachear “no encontrado”, guarda { found: false }; el helper anterior trata null como miss.

Checklist de review

  • Datos personales, permisos y facturación excluidos o justificados
  • Keys completas para locale, tenant, rol, query y versión
  • TTL explicado por frescura de negocio
  • Invalidación después del write exitoso
  • Sin KEYS en producción
  • Lock, jitter o fallback contra stampede
  • Pruebas de key, TTL, hit y miss concurrente
  • Métricas o logs de hit rate

Referencias y siguiente paso

Incluye en las tareas de Claude Code la Redis documentation, node-redis guide, Redis patterns y Redis optimization.

Para convertir esto en flujo de equipo, empieza por productos y plantillas de ClaudeCodeLab. Si necesitas mapear Redis, CDN, writes de DB y observabilidad en un repositorio real, usa formación y consultoría Claude Code.

En el demo Node.js de Masa, la primera petición llamó al loader, la segunda fue Redis hit y dos misses concurrentes ejecutaron el loader una sola vez. El mayor beneficio fue escribir key, TTL, invalidación y criterios de review antes de pedir cambios a Claude Code.

#Claude Code #Redis #caché #Node.js #rendimiento
Gratis

PDF gratis: cheatsheet de Claude Code

Introduce tu email y descarga una hoja con comandos, hábitos de revisión y flujos seguros.

Cuidamos tus datos y no enviamos spam.

Masa

Sobre el autor

Masa

Ingeniero enfocado en workflows prácticos con Claude Code.