Monitoramento18 de outubro de 2026 9 min de leitura

Porque um /health que devolve 200 não chega em serverless

O endpoint de saúde responde 200 enquanto a fila cresce, as novas tentativas se acumulam e a fila de mensagens mortas enche. Vemos que falhas um ping não deteta em serverless e o que fazer em vez disso: um contrato de saúde por componente, vigiar os recursos da nuvem, heartbeats para os crons e fluxos de ponta a ponta que indicam o passo partido.

Este artigo também está disponível em:esenfr

Quase todos os serviços têm um /health. Um monitor chama-o a cada minuto, recebe um 200 e pinta um círculo verde. Num servidor tradicional isso dizia alguma coisa: se o processo responde, o processo está vivo. Em serverless, o endpoint de saúde costuma ser apenas mais uma função, que não partilha processo nem memória com as que fazem o trabalho a sério. Pode responder 200 durante horas enquanto as encomendas ficam paradas numa fila. Neste artigo vemos que falhas lhe escapam, o que fazer em vez disso e como o AAA Sentinel resolve o problema.

O que um 200 verifica de facto

Este é o /health mais habitual numa API sobre AWS Lambda. Se responde, sabem-se três coisas: que o API Gateway encaminha, que essa função arranca e que o seu código não lança uma exceção. Nada mais. Não tocou na base de dados, não sabe se a fila avança e não faz ideia de se a função que consome essa fila está a falhar há uma hora.

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

Em serverless, o endpoint de saúde e o código que processa os dados são funções diferentes, cada uma com as suas permissões, limites de concorrência e erros. Uma estar saudável não diz nada sobre a outra.

Cinco falhas que um 200 não vê

São as que mais se repetem em arquiteturas orientadas a eventos, e nenhuma altera a resposta do /health:

  • A fila que cresce: o consumidor é mais lento do que aquilo que entra, ou está limitado pela concorrência. A API aceita encomendas e responde 202; o trabalho é feito com meia hora de atraso.
  • As novas tentativas silenciosas: uma invocação assíncrona de Lambda que falha é repetida, por omissão, até mais duas vezes. Uma mensagem de SQS que não é apagada volta a ficar visível quando expira o tempo de visibilidade e é processada de novo. O sistema parece funcionar, com o dobro ou o triplo do trabalho e do custo.
  • A fila de mensagens mortas que enche: quando uma mensagem esgota as receções (maxReceiveCount), o SQS move-a para a DLQ. Ninguém dá por isso até alguém a abrir, e cada mensagem lá dentro é algo que não aconteceu.
  • O evento que ninguém consome: o PutEvents do EventBridge devolve sucesso mesmo que nenhuma regra corresponda ao evento. Basta mudar o nome do campo source ou desativar uma regra, e os eventos perdem-se sem um único erro.
  • A dependência em baixo: o fornecedor de pagamentos ou a base de dados falham, mas o /health não os chama, por isso continua verde.

Primeira peça: um contrato de saúde por componente

Se o endpoint de saúde vai existir, que diga algo útil. Há um formato para isso: health+json, um rascunho do IETF que usa o tipo application/health+json. A resposta leva um status global (pass, warn ou fail), a version do serviço e um objeto checks com uma lista por componente, cada um com o seu componentType, o seu observedValue, o seu status e a hora da medição (time). A chave de cada check costuma seguir a 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: o componente funciona.
  • warn: funciona, mas algo não está bem. Aqui, há três mensagens na fila de mensagens mortas. O serviço está degradado, não em baixo, e responde 200.
  • fail: o componente não funciona. O serviço responde 503 para que um balanceador de carga ou um monitor HTTP básico também o percebam.

Como gerá-lo sem escrever o formato à mão

O pacote @aaa-sentinel/health do npm, sem dependências, monta essa resposta a partir das verificações. Cada uma recebe um AbortSignal que dispara quando o tempo se esgota; o estado global é o pior de todos. Este handler produz exatamente a resposta 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 },
  )
  • Uma verificação que lança um erro ou demora mais do que timeoutMs (2 s por omissão, 60 s no máximo) é um fail desse componente; o endpoint continua a responder.
  • As mensagens de erro ficam fora da resposta, a menos que se passe exposeErrors: true. Costumam levar hosts ou cadeias de ligação, e os endpoints de saúde costumam ser públicos.
  • minimumStatus: 'warn' mantém o serviço em warn ou pior, por exemplo durante uma manutenção.
  • Quem já usa Spring Boot Actuator, ASP.NET Core Health Checks ou os endpoints do Kubernetes não precisa de mudar nada: o monitor de contrato de saúde do AAA Sentinel lê-os tal como estão.

Verifique-se o que só o código sabe: que a tabela responde com as permissões certas, que a chave do fornecedor continua válida. O que a nuvem já mede, lê-se da nuvem; é a peça seguinte.

Segunda peça: vigiar os recursos que estão por trás

Há coisas que um endpoint não consegue medir bem. A idade da mensagem mais antiga de uma fila, por exemplo, não é devolvida pelo GetQueueAttributes: está na métrica ApproximateAgeOfOldestMessage do CloudWatch. E é melhor sinal do que o número de mensagens, porque mil mensagens podem ser um pico normal e uma mensagem com quarenta minutos é sempre um problema. Por isso o AAA Sentinel tem monitores de recurso cloud: são passivos, não chamam nada do lado do cliente e leem as métricas através da ligação à nuvem que já foi dada para os custos.

Monitor de recurso cloud · o que se vigia na AWS

Fila SQS
Idade da mensagem mais antiga e mensagens na sua fila de mensagens mortas (qualquer mensagem lá já esgotou as tentativas)
Função Lambda
Erros, throttles e idade do iterador (IteratorAge) quando lê de um stream
Tabela DynamoDB
Leituras e escritas limitadas
Regra do EventBridge
Invocações falhadas para os seus destinos
Step Functions
Execuções falhadas ou com o tempo esgotado
API Gateway
Erros 5XX e de integração
  • No Azure vigia Function Apps, Service Bus, Cosmos DB, API Management, tópicos do Event Grid e Logic Apps; no Google Cloud, Cloud Run, Cloud Functions, subscrições do Pub/Sub, triggers do Eventarc, Workflows, Firestore e API Gateway.
  • O custo vai para a fatura do cliente, não para a nossa: na AWS, o GetMetricData fica em até cerca de 0,20 USD por recurso e por mês com leituras a cada 10 minutos. No Azure costuma caber no milhão de consultas gratuitas por mês, e no Google Cloud, no milhão de séries lidas gratuitas por mês.

Terceira peça: heartbeats para os crons

Uma tarefa agendada não tem endpoint para sondar. Se o agendamento do EventBridge Scheduler for desativado ou o script morrer a meio, não há erro para ver: simplesmente não acontece nada. A solução é inverter o sentido. O monitor de heartbeat dá uma URL; a tarefa chama-a quando termina bem e, se a chamada não chegar dentro da janela definida, o monitor passa a em baixo. Aceita GET e POST, porque muitos agendadores só sabem pedir uma 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) })
}

A URL deve ser chamada só quando a tarefa terminou bem, nunca no início. Um heartbeat no início diz que o cron arrancou, não que exportou a faturação.

Quarta peça: fluxos que indicam o passo partido

As três peças anteriores olham para componentes isolados. Um fluxo olha para a cadeia inteira: o AAA Sentinel dispara uma transação de teste (uma mensagem de SQS, uma invocação de Lambda, um evento do EventBridge ou uma chamada HTTP) com um traceId de 32 caracteres hexadecimais e um traceparent W3C, e verifica que chega a cada passo dentro do prazo. Um passo pode ser um item do DynamoDB, um objeto do S3, uma linha no CloudWatch Logs ou uma chamada do código do cliente a uma URL nossa. Há de 1 a 5 passos, cada um com um prazo entre 10 segundos e 15 minutos, e cada salto é cronometrado a partir do anterior. Os fluxos funcionam hoje na AWS e executam-se, no máximo, a cada 15 minutos, com um teto de custo mensal por fluxo.

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 um fluxo de encomendas que parte no passo 2

Disparo
Mensagem na fila orders, enviada às 09:30:00
Passo 1 · item ORDER#{traceId} no DynamoDB
Correto, 4 s após o disparo
Passo 2 · linha «order stored {traceId}» no CloudWatch Logs
Não chegou dentro do prazo de 60 s
Passo 3 · objeto invoices/{traceId}.pdf no S3
Não verificado
  • O código passa o traceId ao passo seguinte, no cabeçalho x-aaa-trace ou como atributo da mensagem. O propagateTrace encontra-o em eventos de SQS, SNS, EventBridge, DynamoDB Streams, Kinesis e API Gateway ou nos cabeçalhos de um pedido HTTP, e nunca lança exceções.
  • Só a marca do AAA Sentinel transforma um pedido num teste: um traceparent do próprio OpenTelemetry é tráfego real.
  • O fluxo usa um papel à parte, com o seu modelo de CloudFormation, que só pode escrever nos recursos escolhidos pelo cliente.
  • O evento que ninguém consome aparece aqui: o disparo corre bem e o primeiro passo nunca chega.

Como o AAA Sentinel decide que algo está em baixo

Um alerta falso às três da manhã ensina a equipa a ignorar os seguintes. Por isso, uma falha não alerta à primeira. Cada monitor passa por estes estados: operacional, suspeito, a confirmar, em baixo, a recuperar e operacional outra vez. A partir de suspeito nunca se avisa. Um URL volta a ser verificado a partir de uma segunda região da AWS (as sondas saem de eu-west-1 e a confirmação de eu-central-1); um recurso ou um fluxo tem de falhar várias vezes seguidas. Só o estado em baixo abre um incidente e envia o alerta, por e-mail, Slack, Microsoft Teams ou webhook assinado, com os mesmos canais e políticas dos alertas de custo.

  • Um warn de health+json (ou um Degraded de ASP.NET) deixa o serviço degradado, não em baixo. Um UNKNOWN do Actuator não é uma falha.
  • Os monitores novos passam 24 horas a aprender: observam, não alertam e propõem limiares.
  • Os incidentes abrem e fecham sozinhos, com cronologia, notas, reconhecimento e um custo por minuto definido pela organização, na sua moeda.
  • Cada sonda leva o cabeçalho X-AAA-Signature, assinado com um segredo da organização, para que a API a reconheça. Não publicamos IP fixos, por isso a assinatura é a forma de a identificar.

O que não faz

Para que se saiba onde estão os limites:

  • Não é um APM: não ingere logs, não faz rastreio distribuído, não guarda métricas próprias nem tem painéis à medida.
  • No Azure, o assistente de fluxos não descobre a cadeia: você indica onde o teste entra. No Google Cloud, o disparo entra por Pub/Sub ou HTTP.
  • Os pacotes são para Node.js (@aaa-sentinel/health) e Python (aaa-sentinel-health). Em outras linguagens, o seu código lê a marca x-aaa-trace ou aaaTraceId por conta própria.
  • Sonda a partir de duas regiões da AWS, não a partir de uma rede de localizações espalhadas pelo mundo.

Checklist para começar

Com estas peças no lugar, um círculo verde volta a significar que as encomendas chegam ao destino.

  • O /health responde em health+json com um check por dependência, warn para o degradado e 503 para o que está em baixo.
  • Cada fila tem a sua DLQ e um monitor sobre a idade da mensagem mais antiga.
  • As funções que consomem eventos têm os erros e os throttles vigiados.
  • Cada cron chama a sua URL de heartbeat quando termina bem.
  • A cadeia que mais importa (a encomenda, o pagamento, a fatura) tem um fluxo que indica o passo que parte.
  • Os 3 monitores http ou de heartbeat sem custo de qualquer plano cobrem o /health e o cron mais crítico; o pack de monitorização acrescenta o resto.