From n00b to ZeroCool / Profesionalización

Caché: hacer trampa para ir más rápido (sin romper production)

Guía práctica de caché: qué cachear, patrones, TTL e invalidación. Ejemplos con Redis y HTTP, más errores reales y cómo salir.

Lo que vale la pena leer aquí

La escena: viernes 6:40pm. Ya traes la mochila medio lista y tu laptop ya pide jubilación, pero el dashboard se pone rojo. Latencia p95 en 2.8s, CPU del app server a 90% y soporte en el chat: “se tarda un buen en cargar, ¿ya se cayó?”.

La escena: viernes 6:40pm. Ya traes la mochila medio lista y tu laptop ya pide jubilación, pero el dashboard se pone rojo. Latencia p95 en 2.8s, CPU del app server a 90% y soporte en el chat: “se tarda un buen en cargar, ¿ya se cayó?”.

Tú juras que “no se movió nada”… hasta que te acuerdas: marketing soltó campaña y ese endpoint que era bien inocente ahora es el cuello de botella.

La caché es la trampa legal del backend: guardas el resultado de algo caro para no recalcularlo cada vez. Funciona chido… hasta que cacheas lo que no debías, lo invalidas mal o (peor) se lo sirves al usuario equivocado. Ahí es cuando conoces el verdadero Backend Underworld.

Qué te vas a llevar

  • Cómo decidir qué cachear (y qué no) con criterio de negocio y de ingeniería.
  • Patrones que sí se usan en jale real: cache-aside, write-through, stale-while-revalidate.
  • Cómo poner TTL, diseñar keys que no den pena y usar versionado para invalidar sin llorar.
  • Caché por capas: in-process, Redis, HTTP/CDN.
  • Errores comunes (de esos que terminan en “urge hotfix”) y cómo salir del hoyo.

Por qué la caché te salva… y por qué también te puede traicionar

Un request lento casi siempre es combo de cosas:

  • Queries a DB que hacen full scan (o el clásico N+1 escondido),
  • Llamadas a servicios externos (pagos, mapas, recomendaciones),
  • Transformaciones pesadas (agregaciones, reportes, renders),
  • “Pequeñas” cosas repetidas miles de veces por minuto.

La caché brilla cuando:

  • El dato se repite mucho (catálogo, config, perfiles, permisos, feature flags).
  • Recalcular cuesta más que leer de caché.
  • Aguantas que el dato esté poquito desfasado (segundos/minutos), o tienes forma de invalidar.

La caché es peligrosa cuando:

  • El dato es hiper dinámico y crítico (saldo, inventario delicado, “ya pagó o no pagó”).
  • Depende de identidad/roles y puedes filtrar información por una key mal hecha.
  • No tienes historia clara de expiración/invalidez; si no sabes cómo se muere esa entrada, te va a explotar en production.

Antes de cachear, amárralo en una frase:

“Voy a cachear X por Y segundos/minutos para bajar Z (latencia/costo/carga de DB), aceptando que puede estar desactualizado hasta T y se invalida cuando pasa E.”

Si no puedes escribir esa frase, aguanta tantito. Primero arregla query, agrega índice o mata el N+1. Cachear no es sustituto de un bug.

Guía para cachear sin hacerte bolas

1) Encuentra el hot path (no cachees por fe)

Mide con algo real:

  • APM (Datadog/NewRelic) o logs con timing.
  • Métricas: p50/p95/p99, RPS, CPU, conexiones a DB.
  • Top endpoints por tiempo y por volumen.

Tip de guerra: casi nunca duele “lo más lento”; duele lo que tiene más tráfico. Un /search de 300ms a 500 RPS tumba más que un /reports de 2s a 2 RPS.

2) Elige la capa de caché (o combínalas)

a) In-process (memoria del proceso)

  • Pros: ridículamente rápido.
  • Contras: no se comparte entre instancias; se pierde al deploy; ojo con fugas y presión de memoria.
  • Útil para: catálogos chicos, config, resultados cortos.

b) Redis/Memcached (caché distribuida)

  • Pros: compartida, TTLs, tooling, locks, counters.
  • Contras: latencia de red, costo, riesgo de stampede, hay que operarla.
  • Útil para: sesiones, rate limits, queries repetidas, agregaciones.

c) HTTP cache / CDN (edge)

  • Pros: le quita tráfico a tu infra; velocidad brutal.
  • Contras: invalidación a veces lenta/cara; reglas finas con headers; personalización complica todo.
  • Útil para: assets, páginas públicas, endpoints GET públicos.

Regla práctica: si el endpoint es público y GET, empieza por HTTP cache/CDN. Si es interno o personalizado, ve por Redis. Si es un cálculo local chiquito, in-process.

3) Diseña keys que no te metan el pie

Una key de caché es una API. Si la haces fea, la vas a mantener años, con tickets y parches.

Reglas útiles:

  • Namespace por feature: products:..., user:..., search:...
  • Incluye versión: v1, v2 para invalidar “barriendo” sin borrar a mano.
  • Incluye parámetros relevantes y normalizados (orden, minúsculas, defaults).

Ejemplo (búsqueda):

search:v2:q=audifonos&sort=price_asc&page=1&filters=brand:samsung,color:black

Ejemplo (perfil de usuario):

user:v3:id=1234

Escena real: estás en un deploy tarde, el jefe pide “solo un cambio rápido”, y tú debuggeando con redis-cli desde WiFi chafa. Keys legibles te ahorran media vida.

4) Patrón base: cache-aside (el default del mundo real)

Flujo:

  1. La app busca en caché.
  2. Si hay hit, responde.
  3. Si hay miss, consulta DB/servicio, guarda en caché con TTL y responde.

Pseudo-código:

key = "user:v3:id=" + userId
val = cache.get(key)
if val != null:
  return val

val = db.queryUser(userId)
cache.set(key, val, ttl=300) # 5 min
return val

Tradeoff: durante un pico, varios requests pueden hacer miss al mismo tiempo y pegarle a DB igual. Eso es cache stampede. Más abajo lo aterrizamos.

5) TTL con intención (no “ponle una hora y ya”)

Piensa en dos ejes:

  • Volatilidad del dato: ¿cambia cada segundo o cada semana?
  • Costo de estar desactualizado: ¿qué pasa si muestro algo viejo 30s?

Ejemplos pragmáticos:

  • Catálogo (precio/stock cambian): TTL 30–120s, más invalidación por evento si puedes.
  • Perfil/roles: TTL 1–5 min con invalidación cuando cambian permisos.
  • Config/feature flags: TTL 10–60s (o push por pub/sub si ya estás en ese stack).
  • Reportes pesados: TTL 5–30 min y generación async.

Mete jitter para que no expiren miles al mismo tiempo:

ttl = baseTTL + random(0, baseTTL*0.2)

6) Invalidación sin drama: versionado, tags o eventos

La invalidación es el impuesto de usar caché.

A) TTL solamente

  • Fácil, funciona si toleras stale.

B) Versionado en la key (modo supervivencia)

  • Cambias v3 -> v4 y listo.
  • No andas borrando a mano; dejas que lo viejo expire.

C) Invalida por evento

  • Actualizas un producto y borras product:id=... y tal vez listas relacionadas.
  • Preciso, pero más complejo.

D) Tagging / sets (Redis)

  • Guardas relaciones para borrar por grupo.
  • Útil, pero ojo con memoria y con la talacha de mantenerlo.

Si estás profesionalizando un backend ya en producción y no quieres abrir diez frentes: arranca con TTL + versionado. Ya luego metes invalidación por evento donde sí duela.

Caché: hacer trampa para ir más rápido (sin romper production) - visual explicativa 1
Visual de apoyo: Qué te vas a llevar

7) Evita el cache stampede (cuando vence y todos corren a DB)

Pasa en lanzamientos, quincena, Buen Fin, o cuando un TikTok te manda tráfico. Y sí: el sistema “solo tenía que funcionar”.

a) Lock por key (single flight)

  • Un request detecta miss y toma un lock corto.
  • Los demás esperan tantito o sirven stale.
if cache.get(key) != null: return

if cache.setnx("lock:"+key, "1", ttl=5):
  val = compute()
  cache.set(key, val, ttl=300)
  cache.del("lock:"+key)
  return val
else:
  sleep(50ms)
  return cache.get(key) ?? fallbackCompute()

b) Stale-while-revalidate (sirve viejo, refresca en background)

  • UX estable, DB tranquila.
  • Perfecto para listados y páginas que no son “misión crítica por segundo”.

Idea:

  • Guardas value + softTTL.
  • Si softTTL expiró, respondes el valor viejo y disparas refresh async.

8) Caché HTTP bien usada (cuando tu backend ni debería ver ese tráfico)

Para endpoints GET públicos (ej. /public/products, /blog/posts), usa headers:

  • Cache-Control: public, max-age=60, s-maxage=300, stale-while-revalidate=60
  • ETag o Last-Modified para 304 Not Modified.

Regla de oro: si el response depende del usuario (auth/cookies), cuidado. Usa private o no lo caches en CDN. Un mal header y terminas con fuga de datos.

9) Observabilidad: sin métricas la caché es superstición

Mínimos:

  • Hit rate (% hits vs misses) por endpoint/feature.
  • Latencia de cache get/set.
  • Errores de Redis (timeouts, reconnections).
  • Tamaño/evictions.

Si tu hit rate está en 10% y ya metiste complejidad… probablemente cacheaste algo que no se repite o fragmentaste la key con parámetros innecesarios.

Screenshots sugeridos

  • Gráfica de latencia p95 antes/después (APM o Grafana).
  • Dashboard de Redis: hit rate, evictions, used_memory.
  • Ejemplo de request/response con headers Cache-Control y ETag (DevTools o Postman).
  • Logs de un cache miss/hit (con el key hash o parte del key).
  • Query lenta en DB (EXPLAIN) vs misma ruta con caché (tiempos).

Errores comunes + cómo los desactivas

1) Cachear “por usuario” sin meter el usuario en la key

Síntoma: un usuario ve datos de otro. Pánico, ticket VIP y llamadas.

Solución: incluye userId/tenantId/role en la key o no cachees ese response. En HTTP, evita caché compartida cuando hay auth.

2) TTL eterno “para que pegue más”

Síntoma: “¿por qué el precio no cambia?” y termina en rollback.

Solución: TTL razonable + invalidación por evento donde importa (precios/stock). Y versiona keys para cambios grandes.

3) Stampede en picos

Síntoma: expiró una key popular y tu DB se muere en 30 segundos.

Solución: jitter + locks + stale-while-revalidate.

4) Serialización pesada o valores gigantes

Síntoma: Redis lento, red saturada, latencia sube.

Solución: cachea solo lo necesario (DTO compactos), comprime si aplica, o cachea IDs y arma detalles por partes.

5) No tener plan cuando Redis cae

Síntoma: una caída de caché se vuelve caída total.

Solución: timeouts cortos, circuit breaker y degradación. Mejor servir “sin recomendaciones” que tumbar checkout.

Caché: hacer trampa para ir más rápido (sin romper production) - visual explicativa 2
Visual de apoyo: Por qué la caché te salva… y por qué también te puede traicionar

Checklist final (para que no te agarre la noche)

  • Identifiqué el endpoint/flujo con métricas (no por corazonada).
  • Elegí capa correcta (in-process / Redis / HTTP/CDN).
  • Definí key con namespace + versión + params normalizados.
  • Puse TTL con lógica y agregué jitter.
  • Implementé cache-aside y medí hit rate.
  • Consideré stampede (lock o stale-while-revalidate).
  • Definí invalidación (TTL, versionado o evento).
  • Aseguré que no se cachen datos cruzados por usuario/tenant.
  • Agregué observabilidad: hits/misses, latencia, errores, evictions.
  • Definí qué pasa si la caché falla (timeouts + fallback).

FAQ

1) ¿Qué es mejor: Redis o caché en memoria?

Depende del workflow. Memoria es más rápida pero no se comparte entre instancias y se pierde en deploy. Redis suele ser el default pro para caché compartida y con control.

2) ¿Cuánto TTL le pongo a todo?

No hay número mágico. Arranca con lo que el negocio tolera desactualizado (30s, 5min, 1h) y ajusta con métricas. Si dudas: TTL corto y versionado.

3) ¿Cómo sé si la caché está ayudando?

Hit rate alto en rutas de alto tráfico y reducción visible en latencia/CPU/DB queries. Si subió complejidad y no bajó nada, cacheaste mal (o cacheaste muy tarde en el flujo).

4) ¿Puedo cachear responses completos de mi API?

Sí para GET públicos o poco personalizados. Para respuestas con auth, cuidado: Cache-Control: private o evita caché compartida. Muchas veces conviene cachear piezas (catálogo, permisos) en vez del response completo.

5) ¿Qué hago si Redis se cae y todo se vuelve lento?

Timeouts cortos al cliente, circuit breaker y fallback a DB (con protección). La caché debe ser acelerador, no dependencia frágil.

Siguiente episodio

Toca hablar de colas y jobs: cómo sacar trabajo pesado del request sin convertir tu sistema en una piñata de retries. Te vas a topar con eventual consistency y con el arte de dejar de hacer todo “en caliente” por miedo a que falle.