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,v2para 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:
- La app busca en caché.
- Si hay hit, responde.
- 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 -> v4y 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.

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=60ETagoLast-Modifiedpara 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-ControlyETag(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.

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.
Idea para cerrar bien este post: toma una sola práctica de aquí y conviértela en algo que tu equipo pueda aplicar hoy.
Cuando un artículo aterriza en decisiones reales, deja de ser contenido y se vuelve ventaja.


