Una estampida de caché ocurre cuando muchas solicitudes pierden la misma clave caducada y regeneran los datos a la vez. Esta guía explica cómo prevenirla en Laravel con bloqueos atómicos, stale-while-revalidate, TTL jitter y respuestas de respaldo.
Una estampida de caché ocurre cuando muchas solicitudes concurrentes descubren que la misma entrada popular ha caducado o no existe y todas intentan reconstruirla al mismo tiempo. En lugar de proteger la base de datos, la caché dirige hacia ella una ráfaga de trabajo duplicado. El problema también se conoce como thundering herd o efecto manada.
Imagina que una aplicación Laravel guarda durante diez minutos una consulta de «productos más vendidos». Mientras la clave existe, miles de solicitudes reciben la respuesta rápidamente. Cuando caduca y llegan 200 solicitudes a la vez, todas ven un fallo de caché, ejecutan la misma consulta y escriben el mismo resultado. Una operación que debería ejecutarse una vez se ejecuta 200 veces.
La ráfaga puede agotar las conexiones, elevar el uso de CPU, aumentar la latencia y causar timeouts. Las respuestas lentas mantienen más solicitudes abiertas, elevan la concurrencia y pueden convertir una sola clave caducada en un fallo en cascada.
Cache::remember es una excelente opción para el almacenamiento en caché habitual:
$products = Cache::remember('products:top', 600, function () {
return Product::query()
->where('is_active', true)
->orderByDesc('sales_count')
->limit(20)
->get();
});Laravel comprueba la clave, ejecuta el closure si no existe, almacena el resultado y lo devuelve. Sin embargo, la lectura y la regeneración no forman una operación atómica. Si varios workers observan el fallo antes de que el primero termine la consulta, todos pueden entrar en el closure. El código funciona con poco tráfico, pero queda expuesto cuando una clave caliente caduca bajo alta concurrencia.
Caducidad de una clave caliente: una entrada muy solicitada alcanza su TTL en hora punta.
Muchas claves con el mismo TTL: una carga masiva hace que cientos de entradas caduquen juntas.
Vaciado de la caché: un despliegue elimina muchas claves populares a la vez.
Arranque en frío: nuevas instancias reciben tráfico antes de calentar la caché.
Regeneración lenta: consultas costosas o APIs externas dan tiempo a que se acumulen solicitudes.
Fallo del almacén: una interrupción de Redis o Memcached envía demasiado trabajo al origen.
El objetivo es que una sola solicitud regenere el valor mientras las demás esperan brevemente o usan una alternativa. Laravel ofrece bloqueos distribuidos mediante Cache::lock.
use Illuminate\Support\Facades\Cache;
function topProducts()
{
$key = 'products:top';
if ($cached = Cache::get($key)) {
return $cached;
}
return Cache::lock("lock:{$key}", 15)->block(5, function () use ($key) {
// Otro worker puede haber llenado la clave mientras esperábamos.
return Cache::get($key) ?? tap(
Product::query()
->where('is_active', true)
->orderByDesc('sales_count')
->limit(20)
->get(),
fn ($products) => Cache::put($key, $products, now()->addMinutes(10))
);
});
}La segunda comprobación dentro del bloqueo es esencial. Sin ella, cada solicitud en espera obtendría el lock por turnos y recalcularía el valor, aunque la primera ya lo hubiera actualizado. Este patrón se llama double-checked locking.
El tiempo de vida del lock debe superar la duración normal de la regeneración, pero seguir siendo limitado por si un worker falla. Define también una espera corta con block y decide qué devolver cuando se produzca LockTimeoutException: datos antiguos, una respuesta reducida o un error controlado.
Cuando unos minutos de retraso son aceptables, stale-while-revalidate ofrece una experiencia excelente. Cache::flexible define una ventana fresca y otra obsoleta:
$products = Cache::flexible(
'products:top',
[300, 900],
fn () => Product::query()
->where('is_active', true)
->orderByDesc('sales_count')
->limit(20)
->get()
);Durante los primeros 300 segundos Laravel devuelve el valor fresco. Entre 300 y 900 segundos entrega el valor antiguo de inmediato y registra una actualización diferida después de responder. Tras 900 segundos el valor está completamente caducado y debe recalcularse de forma síncrona.
Este enfoque mantiene predecible la latencia y funciona bien para listados de artículos, rankings, paneles, recomendaciones y datos donde una pequeña obsolescencia no causa daño.
Si muchas claves se crean juntas con la misma duración, también pueden caducar juntas. Añade un pequeño desplazamiento aleatorio, llamado TTL jitter, para repartir las actualizaciones:
$ttl = now()->addSeconds(600 + random_int(0, 120));
Cache::put("product:{$product->id}", $payload, $ttl);El jitter no evita que varias solicitudes compitan por una única clave caliente; complementa los locks o stale-while-revalidate. Su función es impedir una ola sincronizada de caducidades.
Los sistemas con mucho tráfico suelen combinar una clave normal, una copia obsoleta de mayor duración y un bloqueo atómico:
use Illuminate\Contracts\Cache\LockTimeoutException;
use Illuminate\Support\Facades\Cache;
function dashboardStats(): array
{
$key = 'dashboard:stats';
$staleKey = 'dashboard:stats:stale';
if ($value = Cache::get($key)) {
return $value;
}
try {
return Cache::lock("lock:{$key}", 20)->block(2, function () use ($key, $staleKey) {
if ($value = Cache::get($key)) {
return $value;
}
$value = app(StatsService::class)->calculate();
$freshTtl = now()->addSeconds(300 + random_int(0, 60));
Cache::put($key, $value, $freshTtl);
Cache::put($staleKey, $value, now()->addHours(2));
return $value;
});
} catch (LockTimeoutException) {
return Cache::get($staleKey, ['status' => 'temporarily_unavailable']);
}
}Un worker calcula los datos; los demás esperan poco tiempo o reciben la copia anterior. Ajusta los intervalos según el coste de la consulta, el tráfico y la antigüedad aceptable.
Todos los servidores deben usar un almacén compartido compatible con bloqueos atómicos. Laravel admite Redis, Memcached, DynamoDB, base de datos, archivos y otros drivers, pero un despliegue distribuido debe apuntar al mismo almacén central.
Usa un nombre de lock único por recurso, tenant, idioma y combinación de filtros.
Limita la duración del bloqueo para recuperarte de workers caídos.
No concentres locks y consultas en la misma base de datos saturada sin evaluar el impacto.
Deja que la forma con closure libere el bloqueo automáticamente.
Trata explícitamente los resultados vacíos; usa Cache::has o un valor centinela.
Evita un único lock global que serialice tareas independientes.
Busca picos periódicos alineados con los límites del TTL: caída súbita de la tasa de aciertos, muchas consultas SQL idénticas, aumento de conexiones, contención de locks y latencia p95 o p99 elevada. Instrumenta la regeneración y registra su duración, el tiempo de adquisición, los timeouts y si se sirvió una copia obsoleta.
Una prueba útil consiste en borrar una clave caliente y enviar solicitudes concurrentes. Si la protección funciona, solo una solicitud debería ejecutar la operación cara y las demás deberían esperar o recibir el fallback.
Usa Cache::remember para claves económicas o de baja concurrencia.
Usa Cache::lock cuando el cálculo debe ejecutarse una vez.
Usa Cache::flexible cuando los datos ligeramente antiguos son aceptables.
Añade TTL jitter cuando muchas claves se escriben juntas.
Calienta de forma proactiva unas pocas claves críticas y predecibles.
Combina locks, fallback, jitter y métricas en las rutas con mayor tráfico.
No. Un fallo de caché aislado es normal. La estampida aparece cuando muchas solicitudes pierden la misma entrada y repiten el trabajo costoso simultáneamente.
No. Redis ofrece acceso rápido y primitivas de bloqueo, pero la aplicación todavía necesita una estrategia de regeneración.
No. Los locks añaden coordinación y latencia. Protege las claves calientes cuya regeneración pueda sobrecargar el backend.
Usa Cache::flexible si aceptas datos antiguos. Si el cálculo debe ejecutarse una vez, protege el fallo con Cache::lock y vuelve a comprobar la clave dentro del bloqueo.
Una estampida de caché es un problema de concurrencia disfrazado de problema de almacenamiento. No basta con ampliar el TTL: coordina la regeneración, sirve datos antiguos cuando sea seguro, distribuye las caducidades y mide qué ocurre cuando desaparece una clave caliente. Laravel proporciona Cache::lock, Cache::flexible y los drivers necesarios para impedir que una clave caducada se convierta en un incidente de producción.
Todavía no hay comentarios aprobados. Las respuestas nuevas pueden esperar moderación.