¿Qué pasa cuando muchos procesos worker arrancan a la vez y golpean el mismo recurso (base de datos, caché), y cómo evitás ese thundering herd?
Los arranques sincronizados convierten un escalado o deploy rutinario en una caída autoinfligida, a menos que la demanda sobre el recurso compartido se reparta en el tiempo.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
¿Qué pasa cuando muchos procesos worker arrancan a la vez y golpean el mismo recurso (base de datos, caché), y cómo evitás ese thundering herd?
Usa los ejemplos y la explicación de la lección para comprobar tu idea.
Reflexión personal. No se envía a ningún servicio de IA.#Cómo Se Ve una Estampida de Arranque
Un thundering herd (estampida) es lo que pasa cuando una gran cantidad de workers independientes deciden actuar sobre el mismo recurso compartido en (o casi en) el mismo instante, y la capacidad de ese recurso no alcanza ni de cerca para absorberlos a todos a la vez. La variante de "arranque" aparece constantemente en despliegues reales: un autoscaler reacciona a un pico de tráfico y lleva de 2 réplicas a 20 en un solo paso; un reinicio rolling (o un deploy fallido que entra en crash-loop y se recupera) tira abajo y vuelve a levantar todas las réplicas existentes en los mismos pocos segundos; un job programado se dispara en cada réplica al inicio del minuto porque está manejado por el reloj de pared en lugar de un único líder.
Sea cual sea el disparador, la forma del problema es la misma: cada proceso nuevo corre su propio camino de arranque, y ese camino generalmente hace trabajo real contra un recurso compartido — abrir un pool de conexiones a la base de datos, precargar una caché local en memoria consultando la base de datos o un servicio de configuración, registrarse en service discovery, u obtener secretos. Cuando ese camino de arranque corre en 20 procesos dentro del mismo segundo, el recurso compartido recibe 20 veces su carga de régimen permanente, toda en una única ráfaga.
#La Variante de Estampida de Caché
Una falla estrechamente relacionada golpea a una única clave de caché caliente en lugar de a una flota de procesos: una entrada de caché muy solicitada expira, y la siguiente request que falla la recalcula desde la base de datos — una consulta lenta y costosa. Con tráfico real, no es una sola request la que falla; son los cientos o miles de requests concurrentes que en ese mismo instante estaban siendo servidas desde esa clave. Todas fallan juntas, y todas de forma independiente golpean la base de datos con el mismo recálculo costoso al mismo tiempo. Es el mismo patrón que el caso de arranque de procesos — demanda sincronizada golpeando un recurso con límites reales de capacidad — solo que disparado por un borde de TTL en lugar de un evento de despliegue.
#Por Qué Se Vuelve una Cascada y No Solo Lentitud
La parte peligrosa es el bucle de retroalimentación en la parte de abajo del camino sin mitigar: la ráfaga inicial causa latencia real y timeouts, lo que dispara reintentos de los llamadores upstream o de la propia lógica de reintento de la app, y esos reintentos caen sobre el mismo recurso ya sobrecargado, sumando carga en lugar de retroceder. Esto es una tormenta de reintentos (retry storm), y es lo que convierte un pico breve y sobrevivible en una caída sostenida — la base de datos o la caché nunca tienen oportunidad de recuperarse porque los reintentos siguen llegando tan rápido como llegaron las requests originales.
#Mitigaciones
| Técnica | Qué hace | Mejor para |
|---|---|---|
| Delay de arranque con jitter | Cada instancia espera una pequeña cantidad aleatoria antes de hacer su trabajo de inicialización costoso | Repartir N arranques simultáneos de procesos en una ventana de tiempo |
| Rollout escalonado / rolling | El despliegue levanta instancias nuevas de a pocas en lugar de todas juntas, controlado por readiness | Deploys y eventos de escalado |
| Límites de pool de conexiones + conexión perezosa | Limitar el máximo de conexiones por instancia y abrirlas bajo demanda en lugar de todas al arrancar | Reducir el pico de conexiones durante el arranque |
| Coalescencia de requests (single-flight) | Solo un llamador recalcula realmente un valor faltante; los llamadores concurrentes esperan ese mismo resultado | Estampida de caché en una clave caliente |
| Expiración temprana probabilística | Recalcular una entrada de caché un poco antes de que expire realmente, con jitter aleatorio por clave | Evitar que muchas claves expiren al mismo tiempo |
| Stale-while-revalidate | Servir el valor obsoleto de inmediato mientras una request de fondo lo refresca | Lecturas sensibles a la latencia donde datos ligeramente obsoletos son aceptables |
| Elección de líder para el warmup | Exactamente una instancia hace la operación costosa de una sola vez; las demás esperan o la omiten | Precarga de caché, migraciones de esquema, registro único |
| Circuit breakers / bulkheads | Dejar de mandar requests a una dependencia sobrecargada en lugar de encolarse detrás de ella | Evitar la falla en cascada una vez que la estampida ya golpeó |
| Backpressure en el recurso | Un pooler de conexiones (p. ej. PgBouncer) o un rate limiter delante del recurso compartido mismo | Limitar la carga sin importar cuántos llamadores existan |
#La Forma del "Full Jitter"
Una fórmula general útil para repartir tanto reintentos como delays de arranque es "full jitter": esperar una duración aleatoria entre cero y un tope que crece exponencialmente — delay = random(0, min(delayMaximo, delayBase * 2^intento)) — en lugar de un delay fijo o puramente exponencial. Los delays fijos reproducen exactamente la misma estampida en cada ronda de reintento, solo que corrida en el tiempo; el jitter rompe la sincronización en lugar de simplemente moverla.
#Los Readiness Gates También Importan
Una versión más sutil de este problema viene de los health checks en lugar del código de la aplicación: si el readiness probe de cada pod nuevo pasa casi en el mismo momento (porque todos tardaron aproximadamente lo mismo en arrancar), el load balancer empieza a mandarles tráfico de producción completo a todos simultáneamente — la estampida golpea las propias dependencias downstream de la flota, no solo el paso de conexión a la base de datos. Repartir initialDelaySeconds, agregar jitter al propio warmup de un pod antes de reportarse listo, o apoyarse en un maxSurge/maxUnavailable conservador en un rolling update abordan esto controlando cuándo empieza a llegar el tráfico, no solo cuándo arranca el proceso.
🧠 Check your understanding
0/3 · 0/3 answered1. ¿Cuál es la característica definitoria de un thundering herd / estampida de arranque?
2. ¿Por qué una política de reintentos ingenua con backoff fijo puede empeorar una caída por thundering herd en lugar de mejorarla?
3. ¿Qué resuelve la 'coalescencia de requests' (single-flight) en la variante de estampida de caché de este problema?