Una caché cambia frescura y complejidad por velocidad y capacidad. Si nadie puede decir la clave, el dueño, la vida útil y la regla de invalidación, no tienes un diseño de caché. Tienes datos viejos con buena latencia.
🎙️ Publicado y grabado: ·
La fuente de verdad es dueña del valor. Una caché guarda una copia más barata de leer. Todo contrato de caché útil responde cuatro preguntas: qué petición exacta se mapea a una clave, cuándo la copia deja de ser aceptable, quién la elimina o la reemplaza, y qué pasa cuando la caché no está disponible. La velocidad es la parte fácil. El contrato es la ingeniería.
request → key → cached value
↘ miss → source of truth → fill cache → response
Useful latency sketch:
in-process memory microseconds
same-region Redis often under a few milliseconds
database query depends on indexes, load, and network
remote API usually the slowest and least predictable
No caches porque una función se vea costosa. Mide las lecturas repetidas, la latencia de la fuente, el tamaño del valor, la frecuencia de actualización y el costo de estar desactualizado. Una consulta indexada de cinco milisegundos que se llama dos veces por minuto no necesita Redis. Agregarle una caché en red puede hacerla más lenta y más difícil de razonar.
La caché del navegador evita un viaje por internet. Una red de distribución de contenido evita llegar a tu origen. Un reverse proxy evita trabajo de la aplicación. Una caché compartida como Redis evita trabajo de base de datos o de API entre muchas instancias de la app. Una caché en proceso evita incluso la llamada a Redis, pero cada proceso es dueño de una copia distinta.
browser → CDN → reverse proxy → application memory → Redis → database
Question at each layer:
What work is skipped?
Who shares this copy?
How is it invalidated?
Can private data leak between users?
What is the failure behavior?
Usa la capa más baja que elimine el trabajo costoso sin multiplicar copias que no puedas invalidar. Los metadatos del catálogo de productos pueden ir en una caché compartida. Un objeto de configuración ya parseado puede ir en la memoria del proceso. El HTML específico de un usuario no debe entrar a la caché de un CDN público. Apilar todas las capas de caché no es sofisticación; crea varios lugares donde el valor de ayer puede sobrevivir.
Cache-Control: public, max-age=3600, así que el navegador y el CDN se quedaron con sus propias copias. Purgar Redis tocó la capa equivocada. Revisa Age, Via y los headers de estado de caché antes de borrar nada.Dos peticiones pueden compartir un valor en caché solo si toda entrada que puede cambiar la respuesta está representada en la clave. Tenant, permisos del usuario, idioma, moneda, feature flags, filtros de consulta y versión del esquema son entradas que faltan a menudo. Normaliza las entradas equivalentes para que reordenar parámetros de query no genere duplicados inútiles.
# Safer, inspectable key
product:v3:tenant_42:sku_9001:locale_en-GB:currency_GBP
# Dangerous: tenant and permission scope are absent
product:sku_9001
# Version the serialized shape during deployment
user-card:v7:{tenantId}:{userId}
Versionar la clave es la invalidación más limpia para cambios de formato. Despliega lectores que entiendan la versión siete, escribe versión siete y deja que la seis expire. No deserialices bytes viejos hacia una forma nueva de objeto esperando que los campos opcionales lo cubran. Mantén las claves legibles hasta que la cardinalidad o su longitud demuestren que hace falta hashear.
Un tiempo de vida limita cuánto puede quedarse una copia sin otra decisión. No garantiza que se refresque al vencer. La expiración normalmente elimina o ignora el valor; la siguiente petición paga el miss. Elige la vida útil según el desfase tolerado y la frecuencia de actualización, no según un número bonito como una hora.
hard TTL: 10 minutes # value cannot be served after this
soft TTL: 8 minutes # refresh in background after this
negative TTL: 15 seconds # brief cache for “not found”
stored value = {
data,
refreshedAt,
softExpiresAt,
hardExpiresAt
}
La caché negativa puede proteger una base de datos cuando los bots piden identificadores que no existen, pero mantén su vida útil corta. Un objeto recién creado no debe quedar invisible detrás de un "no encontrado" cacheado. Agrega jitter aleatorio al TTL para que millones de entradas escritas por un mismo batch no expiren en el mismo segundo.
Cache-aside es el valor por defecto práctico. Las lecturas consultan la caché y, si hay miss, cargan y rellenan. Las escrituras hacen commit en la base de datos y después borran la clave relacionada. Borrar suele ser más seguro que actualizar, porque la siguiente lectura reconstruye desde el estado autoritativo. El orden peligroso es borrar antes del commit.
# Read path
value = cache.get(key)
if value is None:
value = database.load(id)
cache.set(key, serialize(value), ttl=600)
return value
# Write path
database.transaction(() => updateProduct(product))
cache.delete(productKey(product.id)) # only after commit
Esta es la carrera: la petición A borra la caché, la petición B falla el hit y lee la fila vieja, la petición B rellena el valor viejo, y luego la petición A hace commit de la fila nueva. La copia vieja ahora sobrevive un TTL completo. Haz commit primero y borra después. Para garantías más fuertes, escribe un evento de outbox en la misma transacción de base de datos y deja que un worker invalide desde ese evento durable.
KEYS product:* puede bloquear un servidor Redis grande mientras recorre todo el keyspace. Usa namespaces versionados para cortes amplios, mantén conjuntos explícitos de dependencias, o itera con SCAN fuera del camino de la petición. Un FLUSHALL global no es una estrategia de invalidación.Cuando una clave popular expira, muchos workers pueden fallar el hit al mismo tiempo y reconstruir todos el mismo valor. Eso es una estampida de caché. Suele verse como un pico en la base de datos exactamente cuando la caché debía protegerla. Colapsa el trabajo concurrente con un lock de single-flight, refresca los valores populares antes de la expiración dura y agrega jitter entre claves.
value = cache.get(key)
if value is fresh: return value
if lock.tryAcquire("refresh:" + key, ttl=10s):
try:
value = source.load()
cache.set(key, value, ttl=random(540s, 660s))
return value
finally:
lock.release()
return staleValueIfSafe() # or wait briefly, with a deadline
El lock necesita expiración para que un refrescador caído no bloquee la clave para siempre. La llamada a la fuente necesita un timeout más corto que la vida del lock. La liberación debería verificar la propiedad del lock, normalmente con un token aleatorio y un script atómico. Para contenido seguro, stale-while-revalidate le da al usuario una respuesta vieja rápida mientras un worker refresca.
FATAL: remaining connection slots are reserved for non-replication superuser connections después de una expiración masiva no se arregla subiendo primero el límite de conexiones de la base de datos. Uno: identifica las claves sincronizadas y el pico de misses. Dos: limita la concurrencia del relleno de caché. Tres: restablece con TTLs con jitter. Cuatro: agrega refresco single-flight. Cinco: haz prueba de carga del camino con caché frío antes de cambiar la capacidad de la base de datos.Usa la caché de HTTP antes de inventar almacenamiento del lado del cliente. Las directivas de frescura le dicen a los navegadores y a las cachés compartidas si pueden reutilizar una respuesta. Los validadores permiten a una caché vieja preguntar si su copia todavía coincide. Un ETag identifica una representación; Last-Modified usa tiempo y es menos preciso.
# Public fingerprinted asset
Cache-Control: public, max-age=31536000, immutable
# Private account page; browser may store, CDN may not
Cache-Control: private, max-age=60
ETag: "account-42-v19"
Vary: Accept-Encoding
# Revalidation
If-None-Match: "account-42-v19"
HTTP/1.1 304 Not Modified
No le pongas una vida útil de un año a app.js salvo que su URL cambie cuando cambien sus bytes. Usa una huella de contenido como app.a81c9f.js. Usa no-store para respuestas que no deben guardarse. no-cache no significa no almacenar; significa revalidar antes de reutilizar.
curl -I dos veces y compara Age, ETag, Cache-Control, Vary y los headers de estado de caché del proveedor. Prueba autenticado y anónimo. Si el contenido varía por cookies o autorización, demuestra que la clave de caché también varía de forma segura.Redis soporta strings, hashes, sets, sorted sets, streams y más. El comando debe corresponder al tipo guardado. Pon expiraciones a las entradas de caché, limita la memoria a propósito, elige una política de evicción acorde a la carga y mide el tamaño serializado. Una caché sin presupuesto de memoria es una caída agendada por el crecimiento del tráfico.
# Inspect before changing
TYPE user:42
TTL user:42
MEMORY USAGE user:42
# Atomic write with expiry
SET product:42 '{"name":"Mug"}' EX 600
# Typical raw failure
WRONGTYPE Operation against a key holding the wrong kind of value
TYPE key contra la misma base de Redis y el mismo entorno que usa la app que falla. Dos: revisa el comando; GET necesita un string, mientras que HGET necesita un hash. Tres: averigua qué despliegue escribió el tipo en conflicto. Cuatro: despliega un nombre de clave versionado, como user:v2:42. Cinco: deja expirar la clave vieja o bórrala tras confirmar que ya no quedan lectores antiguos. Borrar la clave a ciegas puede hacer que un escritor viejo recree el conflicto.Nunca uses la disponibilidad de Redis como precondición para lecturas ordinarias, salvo que el producto realmente lo exija. Pon un timeout de caché corto, cae de vuelta a la fuente con concurrencia acotada y evita tormentas de reintentos. Si ese fallback aplastaría la base de datos, rechaza o degrada parte del trabajo en lugar de fingir que la caché es opcional.
El ratio de aciertos por sí solo puede mentir. Diez millones de hits sobre objetos baratos pueden esconder una clave costosa que falla una y otra vez. Mide tasa de peticiones, hits, misses, latencia de carga, errores, evicciones, memoria, tamaño de valor y antigüedad al servir. Desglosa solo por dimensiones acotadas, como nombre de caché y resultado, nunca por clave cruda ni identificador de usuario.
cache_requests_total{cache="product",result="hit"}
cache_requests_total{cache="product",result="miss"}
cache_load_duration_seconds{cache="product"}
cache_value_age_seconds{cache="product"}
cache_evictions_total{cache="shared_redis"}
# Failure drill
1. Add latency to cache calls
2. Make cache unavailable
3. Start with an empty cache
4. Expire the hottest keys together
5. Confirm source limits and degraded behavior
Prueba a propósito los arranques en frío y las caídas de caché. Un fallback que nunca vio tráfico a escala de producción es una teoría. Define timeouts explícitos de conexión y de comando. Deja la salud de la caché fuera del readiness básico de la aplicación si un fallo transitorio de caché reiniciaría todas las instancias y amplificaría el incidente.
Escribe esta información junto al código que crea la caché, no en un diagrama que nadie actualiza. Quien depure datos viejos a las dos de la mañana necesita la clave exacta y las reglas de propiedad.
DESIGN
Source of truth: database table / API / computed result
Skipped work: exact query or computation
Key: every input that changes the answer
Value: schema version and maximum size
Freshness: soft TTL, hard TTL, allowed stale age
Invalidation: actor, event, ordering, retry behavior
Failure mode: bypass, stale serve, degrade, or reject
Capacity: memory limit and eviction policy
DEBUG STALE DATA
1. Capture request identity, response, and timestamp
2. Identify browser, CDN, proxy, process, and shared caches
3. Inspect key inputs, stored value, TTL, and value age
4. Compare directly with the source of truth
5. Trace the write commit and invalidation event
6. Purge only the proven layer and key
7. Fix key, ordering, or freshness contract
DEBUG LOAD SPIKE
1. Graph hits, misses, evictions, and load latency
2. Find hot keys and synchronized expiry
3. Bound fill concurrency and source connections
4. Add single-flight refresh and TTL jitter
5. Exercise empty-cache recovery before the next incident
La opinión que conviene guardar es esta: servir datos viejos debe ser una elección explícita de producto. Nombra su antigüedad máxima, deja la clave de caché revisable, invalida después del commit y ensaya el camino del miss. Si esas decisiones no existen, quita la caché hasta que el cuello de botella valga el riesgo.