Health checks em APIs: por que liveness e readiness não são a mesma rota

Uma rota /health que sempre responde 200 parece tranquilizadora até o banco ficar inacessível, uma fila travar ou a aplicação ainda estar aquecendo cache. O processo continua de pé, então a resposta verde aparece. Para quem tentou fazer uma compra naquele instante, porém, o serviço não está saudável de um jeito muito útil.

É por isso que liveness e readiness representam perguntas diferentes. A primeira verifica se a instância deve continuar existindo. A segunda verifica se ela deve receber tráfego. Misturar as duas em uma única rota dá ao orquestrador uma escolha ruim: reiniciar algo recuperável ou manter no balanceador algo incapaz de atender.

Liveness: o processo ainda faz sentido?

Uma liveness probe deve ser barata e conservadora. Ela responde se a aplicação está presa em um estado do qual não deve se recuperar sozinha. Não é o lugar para testar todos os serviços externos. Se um banco tiver uma indisponibilidade de minutos e a liveness falhar por isso, o resultado pode ser uma coreografia de reinícios que não ajuda o banco a voltar.

Readiness: esta instância pode atender agora?

Readiness pode falhar temporariamente sem pedir reinício. É o lugar para reconhecer que a aplicação depende de uma conexão essencial, está carregando dados iniciais ou entrou em modo de manutenção. Quando a resposta não está pronta, o orquestrador remove aquela instância do tráfego até que ela volte a estar apta.

app.get('/live', (_request, response) => {
  response.status(200).json({ status: 'ok' });
});

app.get('/ready', async (_request, response) => {
  const databaseReady = await database.ping();
  response.status(databaseReady ? 200 : 503).json({
    status: databaseReady ? 'ready' : 'not-ready',
  });
});

O exemplo simplifica de propósito. Uma readiness real precisa de timeout curto, cache pequeno para não amplificar carga e uma lista clara de dependências realmente críticas. Um serviço de analytics opcional não deveria derrubar sua capacidade de entregar uma página. Já uma API cuja única função é consultar o banco precisa ser honesta se não consegue falar com ele.

E startup?

Aplicações que levam tempo para iniciar, fazem migrações controladas ou compilam algo no boot podem precisar de uma startup probe. Enquanto ela não passa, liveness e readiness não começam a cobrar a aplicação. Isso evita que um limite de tempo otimista mate repetidamente um processo que só precisava terminar a inicialização.

Por fim, health check não substitui observabilidade. Ele informa um estado para automação; métricas, logs e traces explicam por que aquele estado mudou. A rota de saúde é boa quando dá uma decisão confiável a quem a consome, não quando tenta ser um painel de monitoramento em JSON.

A configuração da probe faz parte do contrato

Uma rota correta ainda pode produzir um sistema instável se o orquestrador consultar rápido demais ou reiniciar na primeira falha. timeoutSeconds, periodSeconds e failureThreshold definem quanto ruído será tolerado antes de uma decisão operacional.

livenessProbe:
  httpGet:
    path: /live
    port: 3000
  periodSeconds: 10
  timeoutSeconds: 1
  failureThreshold: 3

readinessProbe:
  httpGet:
    path: /ready
    port: 3000
  periodSeconds: 5
  timeoutSeconds: 1
  failureThreshold: 2

Os números são ponto de partida, não receita. Meça a latência normal da rota, o tempo de recuperação e quantas instâncias podem sair do balanceador ao mesmo tempo. Se toda readiness consultar o banco a cada segundo, a própria verificação aumenta a carga durante o incidente. Um cache de poucos segundos e timeouts menores que o intervalo evitam probes empilhadas.

Crítico, opcional e degradado não são o mesmo estado

Liste dependências por capacidade. Se o banco é indispensável para qualquer resposta, sua indisponibilidade derruba readiness. Se o provedor de e-mail afeta apenas notificações assíncronas, retire a instância do tráfego seria pior do que atender em modo degradado. Exponha a degradação em métricas e alertas, não como um 503 genérico.

  • não inclua credenciais, hosts internos ou mensagens de exceção na resposta pública;
  • retorne 503 para not-ready e mantenha o corpo pequeno e estável;
  • durante o shutdown, falhe readiness antes de encerrar conexões em andamento;
  • evite migrações destrutivas no startup da mesma instância que recebe tráfego;
  • teste a perda de uma dependência com várias réplicas, não apenas em localhost.

Em Kubernetes 1.37, as novas condições de ciclo de vida do Node acrescentam contexto sobre drain e manutenção, mas não substituem readiness nem executam a retirada de tráfego. Node, Pod e aplicação continuam respondendo perguntas diferentes.

Fontes