La cola invisible: por qué un servicio se pone lento antes de llenarse

Son las tres de la mañana. El p99 de la API pasó de 120 ms a 400 ms. Miro las gráficas y no cuadra nada: CPU al 65 %, memoria estable, sin despliegues en seis horas, cero errores en los logs. El tráfico subió un 15 %. Todo se ve bien y el sitio va lento.
La primera reacción es buscar el bug. Casi nunca hay bug. Lo que pasa es que el panel mide lo que el sistema hace, y el problema está en lo que el sistema espera. Son dos cosas distintas y solo una sale en las gráficas.
La cuenta que conviene tener a mano
La ley de Little dice que L = λ × W. El número medio de cosas dentro de un sistema es cuántas entran por segundo multiplicado por lo que tarda cada una en salir.
Con una cafetería se ve rápido: si entran dos personas por minuto y cada una tarda cinco minutos entre pedir, pagar y recoger, dentro hay diez personas de media. No hace falta saber nada más. Ni cómo llegan, ni si el barista va rápido.
En un servicio se traduce en algo muy usable: concurrencia = throughput × latencia. Si atiendes 500 peticiones por segundo con 40 ms de latencia, tienes 20 peticiones en vuelo de media. Si el pool de conexiones tiene 200, las otras 180 no le están sirviendo a nadie. Están ahí para guardar cola el día que algo se tuerce.
Con dos de los tres números sale el tercero. Es la forma más barata de dimensionar un pool.
El 80 % no significa que vayas por el 80 % del camino
Llamemos utilización (ρ) a la fracción de tiempo que un recurso está ocupado. En la cola más simple que existe, un solo servidor y llegadas al azar, la espera antes de que te atiendan es Wq = ρ / (1 − ρ) × S, donde S es lo que tarda el trabajo en sí.
Toda la historia está en esa fracción:

Del 50 % al 80 % el tráfico sube un 60 % y la espera se multiplica por cuatro. Del 90 % al 95 % el tráfico sube un 5 % y la espera se vuelve a doblar.
La razón se ve pensando en el instante concreto: para que te atiendan de una, el recurso tiene que estar libre justo cuando llegas, y la probabilidad de que lo esté es 1 − ρ. Cuanto más ocupado, menos huecos, y todo lo que llega mientras hay trabajo en curso se pone detrás.
Por eso «vamos al 70 %, nos queda un 30 %» es una frase peligrosa. Ese 30 % no está guardado para después: es lo que mantiene la cola corta. Se puede gastar entero, pero se paga en latencia y se paga mal.
La variabilidad pesa más que la carga
La fórmula de arriba asume que las llegadas están bien repartidas. El tráfico real llega a ráfagas, y la fórmula de Kingman recoge eso: Wq ≈ [ρ / (1 − ρ)] × [(ca² + cs²) / 2] × S.
Son tres cosas multiplicándose: cuán ocupado está el recurso, cuánto varía el ritmo y cuánto tarda el trabajo. En fábricas la llaman la ecuación VUT. El primer factor ya lo vimos, el tercero se entiende solo, y el del medio es el que nadie mide: ca y cs son cuánto varían las llegadas y el tiempo de servicio.
Lo interesante es que dos sistemas con la misma utilización media pueden esperar cosas muy distintas. Y al revés: si bajas la variabilidad a la mitad, bajas la espera a la mitad sin tocar la infraestructura.
¿De dónde sale esa variabilidad en un sistema real?
- Los cron a las 00:00 en punto, y los reintentos disparándose todos a la vez después de un fallo.
- Cachés frías después de un despliegue: el mismo endpoint pasa de 2 ms a 200 ms durante unos minutos.
- Pausas de GC, checkpoints, autovacuums.
- El cliente grande, con cien veces más datos que la media.
- Un N+1 que convierte una petición en trescientas.
Meter jitter en los reintentos, repartir los cron a lo largo del minuto o limitar el tamaño de página suena a detalle menor. En la práctica rinde más que subir de instancia.
Y luego se multiplica
Hasta aquí hablamos de una cola. Una petición de hoy toca veinte: sesión, feature flags, base de datos, caché, dos o tres servicios internos.

Si cada llamada sale lenta una vez de cada cien y la petición abre veinte, la probabilidad de que al menos una salga lenta es 1 − 0,99²⁰, o sea un 18 %. Lo que para cada servicio es «una de cada cien», para el usuario es «una de cada cinco».
De ahí sale la frase que aparece en todos los postmortems: la mediana está perfecta y la gente se queja igual. El p50 lo pone tu servicio. El p99 lo ponen todas las llamadas juntas.
Las colas que no salen en el panel
La otra parte del problema es que las colas no están donde uno mira.

Casi todos los paneles enseñan dos cosas: utilización (CPU, memoria) y tiempo de servicio (cuánto tardó la consulta, cuánto tardó el handler). Ninguna de las dos es la espera. Cuando la base de datos dice que la consulta tardó 3 ms y la aplicación dice que tardó 300 ms, esos 297 ms son cola. Casi siempre, esperar una conexión libre.
Y ojo con qué utilización se mira. La que importa es la del recurso que está haciendo de tapón, no la media de la máquina. Un servidor al 65 % de CPU puede tener un pool de conexiones al 95 % o un lock al 99 %. La media tapa justo lo que está saturado.
Qué hacer
- Medir la espera, no solo el servicio. Dos números por cada salto: cuánto esperó y cuánto tardó. El tiempo que cuesta sacar una conexión del pool es, de lejos, la métrica que más devuelve por lo poco que cuesta ponerla.
- Poner un límite de concurrencia explícito. Un pool pequeño con cola corta y timeout agresivo funciona mejor que un pool enorme. Una cola infinita no evita la caída: cambia errores rápidos por timeouts lentos.
- Rechazar pronto. Si la cola pasa de lo que cabe en el SLO, devuelve 429 o 503. Con la ley de Little es una cuenta y no una opinión: si el presupuesto son 200 ms y cada petición ocupa 5 ms de servidor, no pueden estar esperando más de cuarenta.
- Escalar por espera o por tamaño de cola, no por CPU. La CPU sube tarde, baja pronto y no distingue el trabajo útil del trabajo que espera.
- Bajar la variabilidad antes que subir la capacidad. Jitter, cron repartidos, timeouts por percentil, hedged requests (mandar una copia de la petición al pasar el p95), agrupar escrituras.
- Dejar holgura de verdad. Diseñar el día a día alrededor del 50-70 % del recurso crítico sale más barato de lo que parece: es quedarse en la parte plana de la curva.
El mismo incidente, con números
Volvamos al principio. Una réplica de base de datos responde consultas en 5 ms. A 160 consultas por segundo va al 80 %: la espera son 20 ms y la latencia total, 25 ms. Sube el tráfico un 15 %, a 184 consultas por segundo, y pasa al 92 %: la espera se va a 57 ms y la latencia total, a 62 ms.
Un 15 % más de tráfico, dos veces y media más de latencia. Y la CPU de la máquina en 65 %, porque el tapón nunca fue la CPU. No había bug que encontrar.
Para terminar
La utilización no es una nota que haya que subir. Es una palanca entre lo que cuesta y lo que tarda, y conviene moverla a sabiendas. Cada punto que le quitas a la holgura se lo estás prestando a la cola, y la cola cobra intereses.
Si quieres el recorrido completo de una petición, sin tecnicismos, aquí conté qué pasa entre pulsar Enter y ver la página. Y para seguir por el lado del abanico, lee «The Tail at Scale» en research.google (Dean y Barroso, 2013), que sigue siendo lo mejor escrito sobre el tema. Para la variabilidad, el capítulo de la VUT en Factory Physics de Hopp y Spearman.