Monitorización18 de octubre de 2026 9 min de lectura

Por qué un /health que devuelve 200 no sirve en serverless

Tu endpoint de salud responde 200 mientras la cola crece, los reintentos se acumulan y la cola de fallidos se llena. Te contamos qué fallos no ve un ping en serverless y qué hacer en su lugar: un contrato de salud por componente, vigilar los recursos de la nube, latidos para los crons y flujos de extremo a extremo que nombran el paso roto.

Este artículo también está disponible en:enfrpt

Casi todos los servicios tienen un /health. Un monitor lo llama cada minuto, recibe un 200 y pinta un círculo verde. En un servidor de toda la vida eso decía algo: si el proceso responde, el proceso está vivo. En serverless el endpoint de salud suele ser una función más, que no comparte proceso ni memoria con las que hacen el trabajo de verdad. Puede responder 200 durante horas mientras los pedidos se quedan en una cola. En este artículo repasamos qué fallos se le escapan, qué hacer en su lugar y cómo lo resuelve AAA Sentinel.

Qué comprueba de verdad un 200

Este es el /health más habitual en una API sobre AWS Lambda. Si responde, sabes tres cosas: que API Gateway enruta, que esa función arranca y que su código no lanza una excepción. Nada más. No ha tocado la base de datos, no sabe si la cola avanza y no tiene ni idea de si la función que consume esa cola lleva una hora fallando.

health.tsts
export const handler = async () => ({
  statusCode: 200,
  body: JSON.stringify({ status: 'ok' }),
})

En serverless, el endpoint de salud y el código que procesa tus datos son funciones distintas, con sus propios permisos, límites de concurrencia y errores. Que una esté sana no dice nada de la otra.

Cinco fallos que un 200 no ve

Son los que más se repiten en arquitecturas basadas en eventos, y ninguno cambia la respuesta del /health:

  • La cola que crece: el consumidor va más lento que lo que entra, o está limitado por concurrencia. La API acepta pedidos y responde 202; el trabajo se procesa con media hora de retraso.
  • Los reintentos silenciosos: una invocación asíncrona de Lambda que falla se reintenta, por defecto, hasta dos veces más. Un mensaje de SQS que no se borra vuelve a ser visible al vencer su tiempo de visibilidad y se procesa otra vez. El sistema parece funcionar, con el doble o el triple de trabajo y de coste.
  • La cola de fallidos que se llena: cuando un mensaje agota sus recepciones (maxReceiveCount), SQS lo mueve a la DLQ. Nadie se entera hasta que alguien la abre, y cada mensaje ahí es algo que no ocurrió.
  • El evento que nadie consume: PutEvents en EventBridge devuelve éxito aunque ninguna regla case con el evento. Un cambio de nombre en el campo source o una regla desactivada, y los eventos se pierden sin un solo error.
  • La dependencia caída: el proveedor de pagos o la base de datos fallan, pero el /health no los llama, así que sigue en verde.

Primera pieza: un contrato de salud por componente

Si el endpoint de salud va a existir, que diga algo útil. Hay un formato para eso: health+json, un borrador del IETF que usa el tipo application/health+json. La respuesta lleva un status global (pass, warn o fail), la version del servicio y un objeto checks con una lista por componente, cada uno con su componentType, su observedValue, su status y la hora de la medición (time). La clave de cada check suele seguir la forma componente:medida.

GET /healthhttp
HTTP/1.1 200 OK
Content-Type: application/health+json
Cache-Control: no-store

{
  "status": "warn",
  "version": "1.42.0",
  "checks": {
    "orders-table:responseTime": [
      {
        "componentType": "datastore",
        "observedValue": 38,
        "observedUnit": "ms",
        "status": "pass",
        "time": "2026-10-18T09:30:00.000Z"
      }
    ],
    "orders-dlq:messages": [
      {
        "componentType": "component",
        "observedValue": 3,
        "status": "warn",
        "time": "2026-10-18T09:30:00.000Z"
      }
    ]
  }
}
  • pass: el componente funciona.
  • warn: funciona, pero algo no va bien. Aquí, hay tres mensajes en la cola de fallidos. El servicio está degradado, no caído, y responde 200.
  • fail: el componente no funciona. El servicio responde 503 para que un balanceador o un monitor HTTP básico también lo entiendan.

Cómo generarlo sin escribir el formato a mano

El paquete @aaa-sentinel/health de npm, sin dependencias, monta esa respuesta a partir de tus comprobaciones. Cada una recibe un AbortSignal que se dispara al agotar su tiempo; el estado global es el peor de todos. Este handler produce justo la respuesta anterior:

health.tsts
import { runHealthChecks } from '@aaa-sentinel/health'
import { DynamoDBClient, GetItemCommand } from '@aws-sdk/client-dynamodb'
import { SQSClient, GetQueueAttributesCommand } from '@aws-sdk/client-sqs'

const dynamo = new DynamoDBClient({})
const sqs = new SQSClient({})

// Lambda behind API Gateway or a function URL: the result is the HTTP response.
export const handler = () =>
  runHealthChecks(
    {
      'orders-table:responseTime': async (signal) => {
        const started = Date.now()
        await dynamo.send(
          new GetItemCommand({ TableName: process.env.ORDERS_TABLE, Key: { pk: { S: 'health' } } }),
          { abortSignal: signal },
        )
        return { componentType: 'datastore', observedValue: Date.now() - started, observedUnit: 'ms' }
      },
      'orders-dlq:messages': async (signal) => {
        const { Attributes } = await sqs.send(
          new GetQueueAttributesCommand({
            QueueUrl: process.env.ORDERS_DLQ_URL,
            AttributeNames: ['ApproximateNumberOfMessages'],
          }),
          { abortSignal: signal },
        )
        const messages = Number(Attributes?.ApproximateNumberOfMessages ?? 0)
        return { componentType: 'component', observedValue: messages, status: messages === 0 ? 'pass' : 'warn' }
      },
    },
    { version: process.env.APP_VERSION, timeoutMs: 2000 },
  )
  • Una comprobación que lanza un error o tarda más de timeoutMs (2 s por defecto, 60 s como máximo) es un fail de ese componente; el endpoint sigue respondiendo.
  • Los mensajes de error no salen en la respuesta salvo que pases exposeErrors: true. Suelen llevar hosts o cadenas de conexión, y los endpoints de salud suelen ser públicos.
  • minimumStatus: 'warn' deja el servicio en warn o peor, por ejemplo durante un mantenimiento.
  • Si ya usas Spring Boot Actuator, ASP.NET Core Health Checks o los endpoints de Kubernetes, no hace falta cambiar nada: el monitor de contrato de salud de AAA Sentinel los lee tal cual.

Comprueba lo que sólo tu código sabe: que la tabla responde con tus permisos, que la clave del proveedor sigue siendo válida. Lo que la nube ya mide, léelo de la nube; es la siguiente pieza.

Segunda pieza: vigilar los recursos que hay detrás

Hay cosas que un endpoint no puede medir bien. La antigüedad del mensaje más antiguo de una cola, por ejemplo, no la devuelve GetQueueAttributes: está en la métrica ApproximateAgeOfOldestMessage de CloudWatch. Y es mejor señal que el número de mensajes, porque mil mensajes pueden ser un pico normal y un mensaje de hace cuarenta minutos es siempre un problema. Por eso AAA Sentinel tiene monitores de recurso cloud: son pasivos, no llaman a nada tuyo y leen las métricas a través de la conexión con tu nube que ya diste para costes.

Monitor de recurso cloud · qué se vigila en AWS

Cola SQS
Antigüedad del mensaje más antiguo y mensajes en su cola de fallidos (cualquier mensaje ahí ya agotó sus reintentos)
Función Lambda
Errores, throttles y antigüedad del iterador (IteratorAge) cuando lee de un stream
Tabla DynamoDB
Lecturas y escrituras limitadas
Regla de EventBridge
Invocaciones fallidas hacia sus destinos
Step Functions
Ejecuciones fallidas o con el tiempo agotado
API Gateway
Errores 5XX y de integración
  • En Azure vigila Function Apps, Service Bus, Cosmos DB, API Management, temas de Event Grid y Logic Apps; en Google Cloud, Cloud Run, Cloud Functions, suscripciones de Pub/Sub, triggers de Eventarc, Workflows, Firestore y API Gateway.
  • El coste va en tu factura, no en la nuestra: en AWS, GetMetricData sale por hasta unos 0,20 USD por recurso y mes con lecturas cada 10 minutos. En Azure suele caber en el millón de consultas gratuitas al mes, y en Google Cloud, en el millón de series leídas gratis al mes.

Tercera pieza: latidos para los crons

Un trabajo programado no tiene endpoint que sondear. Si la programación de EventBridge Scheduler se desactiva o el script muere a mitad, no hay error que ver: simplemente no pasa nada. La solución es darle la vuelta. El monitor de latido te da una URL; tu trabajo la llama al terminar bien, y si la llamada no llega dentro de la ventana que defines, el monitor pasa a caído. Acepta GET y POST, porque muchos planificadores sólo saben pedir una URL.

nightly.shbash
#!/usr/bin/env bash
set -euo pipefail

node scripts/nightly-billing-export.js

# Only reached when the job succeeded: a failed or skipped run stays silent.
curl -fsS --max-time 10 --retry 3 "$HEARTBEAT_URL" > /dev/null
scheduled.tsts
// Scheduled Lambda (EventBridge Scheduler): the ping is the last line, after the real work.
export const handler = async () => {
  await exportBilling()
  await fetch(process.env.HEARTBEAT_URL, { method: 'POST', signal: AbortSignal.timeout(5000) })
}

Llama a la URL sólo cuando el trabajo ha terminado bien, nunca al empezar. Un latido al principio dice que el cron arrancó, no que exportó la facturación.

Cuarta pieza: flujos que nombran el paso roto

Las tres piezas anteriores miran componentes sueltos. Un flujo mira la cadena entera: AAA Sentinel dispara una transacción de prueba (un mensaje de SQS, una invocación de Lambda, un evento de EventBridge o una llamada HTTP) con un traceId de 32 caracteres hexadecimales y un traceparent W3C, y comprueba que llega a cada paso dentro de su plazo. Un paso puede ser un ítem de DynamoDB, un objeto de S3, una línea en CloudWatch Logs o una llamada de tu código a una URL nuestra. Hay de 1 a 5 pasos, cada uno con un plazo de entre 10 segundos y 15 minutos, y se mide cuánto tarda cada salto desde el anterior. Los flujos funcionan hoy en AWS y se ejecutan, como mucho, cada 15 minutos, con un tope de coste al mes por flujo.

worker.tsts
import { propagateTrace } from '@aaa-sentinel/health'
import { DynamoDBClient } from '@aws-sdk/client-dynamodb'
import { DynamoDBDocumentClient, PutCommand } from '@aws-sdk/lib-dynamodb'

const db = DynamoDBDocumentClient.from(new DynamoDBClient({}))

export const handler = async (event) => {
  for (const record of event.Records) {
    const trace = propagateTrace(record) // null for real traffic

    if (trace) {
      // Step 1 looks for ORDER#{traceId}; step 2 for the line "order stored {traceId}".
      await db.send(new PutCommand({ TableName: process.env.ORDERS_TABLE, Item: { pk: `ORDER#${trace.traceId}`, test: true } }))
      trace.log('order stored')
      continue // no charge, no email for the test transaction
    }

    const order = JSON.parse(record.body)
    await db.send(new PutCommand({ TableName: process.env.ORDERS_TABLE, Item: { pk: `ORDER#${order.id}`, ...order } }))
    await chargeCustomer(order)
  }
}

Veredicto de un flujo de pedidos que se rompe en el paso 2

Disparo
Mensaje en la cola orders, enviado a las 09:30:00
Paso 1 · ítem ORDER#{traceId} en DynamoDB
Correcto, 4 s desde el disparo
Paso 2 · línea «order stored {traceId}» en CloudWatch Logs
No llegó dentro de su plazo de 60 s
Paso 3 · objeto invoices/{traceId}.pdf en S3
Sin comprobar
  • Tu código pasa el traceId al siguiente paso, en la cabecera x-aaa-trace o como atributo del mensaje. propagateTrace lo encuentra en eventos de SQS, SNS, EventBridge, DynamoDB Streams, Kinesis, API Gateway o en las cabeceras de una petición HTTP, y nunca lanza.
  • Sólo la marca de AAA Sentinel convierte una petición en prueba: un traceparent de tu propio OpenTelemetry es tráfico real.
  • El flujo usa un rol aparte, con su plantilla de CloudFormation, que sólo puede escribir en los recursos que tú eliges.
  • El evento que nadie consume aparece aquí: el disparo sale bien y el primer paso no llega nunca.

Cómo decide AAA Sentinel que algo está caído

Una alerta falsa a las tres de la mañana enseña al equipo a ignorar las siguientes. Por eso un fallo no alerta a la primera. Cada monitor pasa por estos estados: arriba, sospechoso, confirmando, caído, recuperando y arriba otra vez. Desde sospechoso nunca se avisa. Una URL se vuelve a comprobar desde una segunda región de AWS (las sondas salen de eu-west-1 y la confirmación de eu-central-1); un recurso o un flujo tiene que fallar varias veces seguidas. Sólo caído abre un incidente y envía la alerta, por correo, Slack, Microsoft Teams o un webhook firmado, con los mismos canales y políticas que las alertas de coste.

  • Un warn de health+json (o un Degraded de ASP.NET) deja el servicio degradado, no caído. Un UNKNOWN de Actuator no es un fallo.
  • Los monitores nuevos pasan 24 horas aprendiendo: observan, no alertan y proponen umbrales.
  • Los incidentes se abren y se cierran solos, con su línea de tiempo, notas, reconocimiento y un coste por minuto que defines tú, en la moneda de tu organización.
  • Cada sonda lleva la cabecera X-AAA-Signature, firmada con un secreto de tu organización, para que tu API la reconozca. No publicamos IP fijas, así que la firma es la forma de identificarla.

Lo que no hace

Para que sepas dónde están los límites:

  • No es un APM: no ingiere logs, no hace trazas distribuidas, no guarda métricas propias ni tiene paneles a medida.
  • En Azure el asistente de flujos no descubre la cadena: le indicas dónde entra la prueba. En Google Cloud el disparo entra por Pub/Sub o HTTP.
  • Los paquetes son para Node.js (@aaa-sentinel/health) y Python (aaa-sentinel-health). En otros lenguajes, tu código lee la marca x-aaa-trace o aaaTraceId por su cuenta.
  • Sondea desde dos regiones de AWS, no desde una red de ubicaciones repartidas por el mundo.

Checklist para empezar

Con estas piezas, un círculo verde vuelve a significar que los pedidos llegan a su destino.

  • Tu /health responde health+json con un check por dependencia, warn para lo degradado y 503 para lo caído.
  • Cada cola tiene su DLQ y un monitor sobre la antigüedad del mensaje más antiguo.
  • Las funciones que consumen eventos tienen vigilados errores y throttles.
  • Cada cron llama a su URL de latido al terminar bien.
  • La cadena que más importa (el pedido, el pago, la factura) tiene un flujo que nombra el paso que se rompe.
  • Los 3 monitores http o de latido sin coste de cualquier plan cubren el /health y el cron más crítico; el pack de monitorización añade el resto.