Caso técnico anonimizado. Se omiten nombres de cliente, endpoints internos exactos, identificadores, fechas precisas, proveedores y cualquier dato que pueda asociarse con una organización específica.

Hace poco me tocó investigar un incidente de rendimiento en una plataforma que empezó a presentar latencias altas, errores intermitentes y respuestas inconsistentes en uno de sus flujos principales de búsqueda.

La primera hipótesis era bastante común:

"Tal vez necesitamos más servidor."

Y sí, escalar infraestructura a veces ayuda. Pero en este caso, la evidencia apuntaba a algo más interesante: el problema no era simplemente CPU, RAM o tamaño de instancia. El sistema estaba sufriendo por una combinación de dependencias lentas, flujos costosos, timeouts, recursos compartidos y falta de aislamiento operacional.

Dicho de otra forma: no había un solo "culpable". Había una arquitectura que permitía que un flujo pesado afectara a otros flujos que deberían haber permanecido estables.

El síntoma visible #

El cliente reportaba degradación en un flujo crítico de búsqueda. Algunas solicitudes respondían bien, otras tardaban demasiado, otras terminaban en errores y una parte importante era rechazada por mecanismos de protección.

Al revisar logs de proxy, aplicación, scheduler y servicios internos, aparecieron varias señales.

Durante varios días, el endpoint principal de búsqueda estuvo dominado por respuestas tipo 429. Eso indicaba que el sistema estaba aplicando backpressure o rate limiting de forma sostenida.

En otra ventana, la aplicación empezó a registrar miles de timeouts al llamar a un servicio interno de almacenamiento y búsqueda.

Más tarde, el servicio dependiente mostró una señal todavía más clara: el proxy no podía conectarse correctamente con el runtime de aplicación. Eso ya no era una sospecha de lentitud; era evidencia de saturación del pool de workers.

Como dato adicional, también había jobs batch de larga duración ejecutándose en ventanas cercanas al incidente, procesando millones de registros.

Nada de esto, por sí solo, explicaba todo el caso. Pero en conjunto contaba una historia bastante consistente.

Lo que los logs empezaron a mostrar #

La primera señal fuerte fue que el sistema ya venía "defendiéndose" antes del incidente más visible.

Durante varios días, una gran proporción de las solicitudes al flujo principal terminaba en rate limit. Esto es importante porque un 429 no siempre significa que el sistema está sano; a veces significa que el sistema está evitando colapsar.

El rate limiting puede proteger al backend, pero también puede ocultar que las solicitudes que sí entran siguen ejecutando trabajo muy costoso: llamadas a dependencias, búsqueda, serialización de respuestas grandes, acceso a base de datos, deduplicación o fan-out hacia otros servicios.

Después aparecieron los timeouts.

La aplicación esperaba respuesta de un servicio interno, pero esa dependencia no respondía a tiempo. Cuando esto ocurre, los workers de la aplicación quedan ocupados esperando. Si suficientes workers quedan bloqueados, el sistema empieza a perder capacidad aunque la CPU global no esté al 100%.

Ese fue uno de los aprendizajes más importantes del caso:

Un sistema puede verse "bien" en métricas generales de infraestructura y aun así estar degradado a nivel aplicación.

CPU promedio, RAM disponible o carga general no siempre muestran workers bloqueados, pools llenos, conexiones esperando, locks, timeouts o payloads demasiado grandes.

El hallazgo crítico #

La evidencia más clara apareció cuando el servicio dependiente dejó de aceptar conexiones hacia el runtime de aplicación durante unos minutos.

Eso afectó no solo a un endpoint pesado, sino también a endpoints más simples, incluyendo health checks.

Esa parte fue clave.

Cuando incluso el health check falla, normalmente ya no estás viendo "un request lento". Estás viendo que el servicio, o al menos su pool de ejecución, está saturado o no disponible.

En términos prácticos: si tienes pocos workers disponibles y algunos requests lentos los ocupan durante demasiado tiempo, el servicio completo puede quedarse sin capacidad para atender cosas sencillas.

Esto cambia la conversación.

Ya no se trata solo de optimizar una query o subir CPU. Se trata de preguntarse:

¿Puede una lista pesada, un cliente grande, un job batch o una dependencia lenta degradar el resto del sistema?

En este caso, la respuesta era: sí.

El job batch sospechoso, pero no culpable único #

También apareció un job batch de limpieza que había corrido durante muchas horas y procesado millones de filas.

Este tipo de proceso puede generar presión sobre base de datos, índices, I/O, caché y locks. Sin embargo, con la evidencia disponible no era correcto decir:

"El job fue la causa del incidente."

Eso habría sido una conclusión demasiado fuerte.

La formulación correcta era más cuidadosa:

"El job batch es un factor contribuyente de alto riesgo y debe investigarse como posible fuente de contención."

Esta distinción importa mucho en incident response. No todo evento correlacionado es causa raíz. Pero tampoco hay que ignorar procesos masivos que compiten con tráfico online.

La causa raíz no era "la arquitectura está mal" #

Una mala forma de comunicar este tipo de hallazgo sería:

"La arquitectura está mal."

Eso no ayuda, genera resistencia y simplifica demasiado el problema.

La forma más defendible era:

"El diseño actual no aísla suficientemente flujos de diferente costo ni limita el radio de impacto de una dependencia lenta, cliente pesado o job batch."

Esa frase cambia el enfoque.

No se trata de culpar a un componente, a un equipo o a una tecnología. Se trata de identificar una propiedad del sistema: el radio de impacto es demasiado grande.

Un flujo costoso podía consumir recursos compartidos. Una dependencia lenta podía dejar workers esperando. Un job batch podía competir con tráfico interactivo. Y los mecanismos de observabilidad no permitían reconstruir con precisión cada request de extremo a extremo.

Lo que recomendé #

La recomendación principal no fue "compremos más servidor".

Escalar puede comprar tiempo, pero no arregla por sí solo el problema estructural. Si una dependencia lenta puede bloquear workers, más workers solo retrasan el colapso. Si no hay límites de concurrencia por cliente o por tipo de flujo, un caso pesado puede seguir afectando a los demás.

Las acciones prioritarias fueron las siguientes.

1. Mejorar observabilidad #

Agregar tiempos reales por request y por upstream en el proxy, request IDs end-to-end, métricas por fase del flujo y slowlogs del runtime de aplicación.

Sin eso, el diagnóstico depende demasiado de inferencias.

2. Revisar pools y timeouts #

Workers insuficientes, timeouts excesivamente largos y ausencia de deadlines pueden convertir una dependencia lenta en una cascada de saturación.

No basta con saber cuántos recursos tiene el servidor. Hay que saber cuántos requests puede atender el pool cuando parte de ellos queda esperando.

3. Aplicar aislamiento #

Separar flujos rápidos y pesados, usar límites de concurrencia por cliente o por tipo de trabajo, y aplicar patrones como bulkheads y circuit breakers.

La meta es simple: que un componente degradado no arrastre al resto.

4. Controlar jobs batch #

Procesos masivos deben correr con batch size, rate limit, lock timeout, monitoreo y ventanas operativas claras.

El tráfico online y los procesos batch no deberían competir sin control por los mismos recursos críticos.

El aprendizaje principal #

Este caso dejó una lección que se repite mucho en sistemas en producción:

No todo incidente de latencia se resuelve agregando infraestructura.

A veces el problema real está en cómo se comparten los recursos, cómo se manejan las dependencias lentas y qué tan aislados están los flujos caros de los flujos rápidos.

La pregunta clave que me quedó fue esta:

¿Qué tanto daño puede causar un solo cliente, una sola lista, un solo job o una sola dependencia lenta?

Si la respuesta es "puede degradar todo el servicio", entonces el sistema necesita reducir su radio de impacto.

Y esa, más que "más servidor", suele ser la diferencia entre una plataforma que simplemente aguanta carga y una plataforma que realmente es resiliente.