Casos reales/ServerStack Journal

Cuando "healthy" miente: el container respondía sano pero el servicio no funcionaba

Un servicio en producción mostraba el endpoint de salud verde, pero ningún cliente podía usarlo. Este post-mortem muestra los tres problemas anidados que provocaron la falla y las lecciones para diseñar health checks que no mientan.

Un día de abril, el servicio de facturación de una de nuestras plataformas dejó de procesar pagos. La página de precios mostraba "el servicio de pagos está temporalmente fuera de línea". Ningún cliente podía suscribirse.

Lo desconcertante: el endpoint de salud del servicio respondía perfectamente "Healthy". El monitoreo decía verde. El balanceador decía verde. Y aun así, nada funcionaba.

Este es el desglose de los tres problemas anidados que provocaron la falla, y la lección de fondo: los health checks superficiales mienten.

Los tres problemas anidados

Problema 1: la red Docker incorrecta

El servicio de facturación corría en su propio contenedor, en su propia red Docker. La aplicación principal corría en otra red. Aunque ambos contenedores estaban "vivos", no podían hablar entre sí porque no compartían red.

# Lo que faltaba en docker-compose
services:
  billing:
    networks:
      - facturacion_red
      - aplicacion_red   # <-- esta no estaba

En logs aparecían intentos de conexión que terminaban en timeout. El health check, que solo verificaba que el proceso interno estuviera vivo, no detectaba esto.

Problema 2: una columna que se llamaba distinto en mayúsculas y minúsculas

Una migración de base de datos había creado una columna llamada trialendsat. La aplicación esperaba TrialEndsAt (con mayúsculas). En PostgreSQL, los nombres de columna son sensibles a mayúsculas si se crearon con comillas, y nuestra aplicación los consultaba con comillas.

Resultado: cada vez que la aplicación intentaba leer suscripciones, PostgreSQL respondía:

Npgsql.PostgresException: column s.TrialEndsAt does not exist

El servicio "vivía". El servicio "no funcionaba". El health check no se enteraba.

Problema 3: mensajes envenenados en la cola

Como los procesos fallaban con error, los mensajes de eventos (que llegan por RabbitMQ) volvían a la cola para reintentarse. Esto generó mensajes envenenados: mensajes que fallan siempre, se reencolan siempre, y consumen toda la capacidad de procesamiento. Mientras tanto, los mensajes nuevos quedaban atrás esperando.

A simple vista la cola no estaba "caída". Estaba ocupada. Eternamente ocupada. Procesando lo mismo, fallando, reintentando.

Por qué el monitoreo decía "todo bien"

El health check del servicio era el clásico: una ruta /health que respondía 200 OK si el proceso estaba escuchando. Eso es lo que la mayoría llama "healthy". Pero un proceso vivo no es lo mismo que un servicio funcional.

Un servicio realmente funcional necesita:

  1. Que el proceso responda → ✓ (era el único check que teníamos).
  2. Que pueda hablar con su base de datos → ✗ (la columna no existía como esperaba).
  3. Que pueda hablar con sus dependencias por red → ✗ (red Docker faltante).
  4. Que la cola de mensajes no esté ahogada → ✗ (mensajes envenenados).

De los cuatro, solo verificabamos uno. Por eso el monitoreo decía "verde" mientras el servicio agonizaba.

Las correcciones aplicadas

Fix 1: red Docker corregida

Añadimos la red faltante al docker-compose. El servicio recuperó comunicación con la aplicación principal.

Fix 2: nombre de columna alineado

Renombramos la columna en la base de datos para que coincidiera exactamente con lo que la aplicación esperaba, incluyendo mayúsculas. Mejor aún: documentamos en el equipo que las migraciones futuras deben citar el nombre tal como lo usa la aplicación (con comillas) para evitar el problema de origen.

Fix 3: cola limpiada y dead letter queue activada

Eliminamos los mensajes envenenados y configuramos una cola de mensajes muertos. A partir de ahí, si un mensaje falla varias veces, se mueve a una cola separada en vez de bloquear la principal. Eso permite que el procesamiento normal siga avanzando y que los mensajes problemáticos se inspeccionen aparte.

Fix 4 (el más importante): health checks profundos

Cambiamos el health check del servicio. Ahora no solo verifica que el proceso responde, sino:

  • Que la conexión a la base de datos funcione (consulta corta de prueba).
  • Que el broker de mensajes esté conectado.
  • Que las dependencias internas críticas respondan.

Si cualquiera falla, el endpoint de salud devuelve 503 (servicio no disponible). El monitoreo entonces sí avisa.

La lección que importa

Un health check que solo responde 200 OK es teatro. Le dice al monitoreo lo que el monitoreo quiere oír, no lo que está pasando de verdad. La regla: el health check tiene que tocar al menos una dependencia crítica de cada categoría (base de datos, cola, otros servicios).

Si no, vas a vivir el escenario de este post: el dashboard te dice "todo bien" mientras los clientes te escriben molestos porque nada funciona.

Liveness vs readiness — distinción que vale la pena hacer

En entornos modernos (Kubernetes, Docker Swarm) los health checks suelen separarse en dos:

  • Liveness: ¿el proceso está vivo? Si no, reinícialo.
  • Readiness: ¿el servicio puede atender clientes ahora mismo? Si no, no le mandes tráfico.

Esa distinción salva muchas vidas. Si la base de datos está temporalmente caída, el servicio sigue vivo (no quieres reiniciarlo en bucle), pero no está listo para atender clientes. El balanceador deja de enviarle tráfico hasta que se recupere.

La mayoría de stacks que vemos solo tiene "liveness". Eso es la mitad del trabajo.

Cómo lo aplicamos hoy

En los servicios que mantenemos, el patrón es:

GET /health/live      → ¿el proceso está vivo? (rápido, sin dependencias)
GET /health/ready     → ¿puede atender clientes? (toca BD, cola, etc.)
GET /health/startup   → ¿terminó el arranque inicial?

Y el monitoreo externo verifica los tres, con interpretaciones distintas:

  • Si live falla → reinicia el contenedor.
  • Si ready falla → saca el contenedor del balanceador, no le mandes tráfico, alerta.
  • Si startup no devuelve OK en 60 segundos → el arranque está roto, alerta.

En ServerStack Solutions

En cada servicio que ponemos en producción, los health checks son profundos por defecto y el monitoreo distingue entre "el proceso vive" y "el servicio funciona". No es un extra opcional — es lo que evita que un domingo descubras por una llamada de cliente que tu sistema lleva horas roto.

Solicita una revisión de tus health checks actuales. Si tu sistema solo dice "200 OK" para todo, podemos identificar dónde te estás engañando a ti mismo.


Relacionado: Una hora 40 caída por una IP hardcodeada · Microservicios mudos: el día que nadie hablaba con nadie · Microservicios + DDD + Outbox Pattern.

Contando…

Preguntas frecuentes

¿Un health check profundo no es lento?

Si lo haces bien, no. Las consultas de prueba a la base de datos deben ser triviales (un SELECT 1) y los chequeos a colas deben ser conexiones de bajo costo. El health check debe responder en menos de 500ms idealmente. Si tarda más, hay un problema de fondo que el propio chequeo te está revelando.

¿Qué es una dead letter queue y por qué es importante?

Es una cola separada donde van los mensajes que fallan repetidamente. Sin DLQ, un mensaje malo puede bloquear toda la cola principal porque se reintenta infinitamente. Con DLQ, después de N intentos fallidos el mensaje se mueve a la DLQ, la cola principal sigue procesando, y un humano puede inspeccionar el mensaje problemático con calma. Es estándar en RabbitMQ, Kafka y SQS.

¿Cómo sé si mis health checks son superficiales?

Pregúntate: si la base de datos se cae ahora, ¿el endpoint de salud cambia a 503 inmediatamente? Si no, tu chequeo es superficial. Mismo test con: "¿si la cola de mensajes está saturada?" "¿si un servicio crítico del que dependes no responde?". Si el endpoint sigue diciendo 200 OK en cualquiera de esos escenarios, no estás monitoreando el servicio — estás monitoreando que el proceso esté vivo.

Escrito por

Equipo ServerStack Solutions

Fundador, ServerStack Solutions. Fundador de ServerStack Solutions. Diseño infraestructura y automatización para negocios que quieren dormir tranquilos. Escribo sobre CI/CD, DevOps y herramientas que hacen la diferencia.