Health check /health em APIs Node.js

Um endpoint /health bem implementado é a diferença entre descobrir que sua API está com problemas antes ou depois do usuário reclamar. Load balancers, Docker, Kubernetes, Uptime Kuma e Railway usam esse endpoint para decidir se devem rotear tráfego ou reiniciar o container.

Endpoint /health simples e com profundidade

Dois níveis de health check: liveness (está vivo?) e readiness (pode receber tráfego?):

typescript
import express from 'express'
import { Pool } from 'pg'
import { createClient } from 'redis'

const app = express()
const db = new Pool({ connectionString: process.env.DATABASE_URL })
const redis = createClient({ url: process.env.REDIS_URL })

// Liveness: apenas verifica se o processo está respondendo:
app.get('/health/live', (req, res) => {
  res.json({ status: 'ok', timestamp: new Date().toISOString() })
})

// Readiness: verifica dependências críticas:
app.get('/health/ready', async (req, res) => {
  const checks: Record<string, { status: string; latency?: number; error?: string }> = {}
  let overallOk = true

  // Verificar PostgreSQL:
  try {
    const start = Date.now()
    await db.query('SELECT 1')
    checks.database = { status: 'ok', latency: Date.now() - start }
  } catch (err) {
    checks.database = { status: 'error', error: (err as Error).message }
    overallOk = false
  }

  // Verificar Redis:
  try {
    const start = Date.now()
    await redis.ping()
    checks.redis = { status: 'ok', latency: Date.now() - start }
  } catch (err) {
    checks.redis = { status: 'error', error: (err as Error).message }
    overallOk = false
  }

  // Verificar uso de memória:
  const mem = process.memoryUsage()
  const heapUsedMB = Math.round(mem.heapUsed / 1024 / 1024)
  checks.memory = {
    status: heapUsedMB < 400 ? 'ok' : 'warning',
    latency: heapUsedMB,
  }

  res.status(overallOk ? 200 : 503).json({
    status: overallOk ? 'ok' : 'degraded',
    version: process.env.npm_package_version,
    uptime: Math.round(process.uptime()),
    checks,
  })
})

// Alias mais comum:
app.get('/health', (req, res) => res.redirect('/health/ready'))

Integrar com Docker healthcheck

Docker reinicia o container automaticamente quando o healthcheck falha:

dockerfile
# Dockerfile — adicionar healthcheck:
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY dist/ ./dist/

# Healthcheck nativo do Docker:
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
  CMD wget -qO- http://localhost:3000/health/live || exit 1

# (ou com curl se estiver disponível):
# HEALTHCHECK CMD curl -f http://localhost:3000/health/live || exit 1

USER node
CMD ["node", "dist/index.js"]

# docker-compose.yml:
services:
  api:
    build: .
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost:3000/health/live"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 20s
    # Traefik ou Nginx só roteia para containers healthy:
    # status healthy → container recebe tráfego

# Ver status do healthcheck:
docker inspect --format='{{.State.Health.Status}}' nome-do-container
docker ps --filter health=healthy

Health check com Fastify

Implementação para APIs Fastify com plugin dedicado:

typescript
// npm install fastify @fastify/under-pressure

import Fastify from 'fastify'
import underPressure from '@fastify/under-pressure'

const app = Fastify({ logger: true })

// Plugin under-pressure: monitora event loop lag:
await app.register(underPressure, {
  maxEventLoopDelay: 1000,    // ms — degradado se event loop atrasar > 1s
  maxHeapUsedBytes: 500_000_000,  // 500 MB
  maxRssBytes: 750_000_000,
  exposeStatusRoute: {
    routeOpts: { url: '/health' },
    routeSchemaOpts: { tags: ['health'] },
  },
  healthCheck: async (fastifyInstance) => {
    // Verificações customizadas — retornar false = unhealthy:
    try {
      await fastifyInstance.db.query('SELECT 1')
      return true
    } catch (err) {
      return false
    }
  },
  healthCheckInterval: 15_000,  // verificar a cada 15s
})

// Resposta do under-pressure:
// { status: 'ok' } → 200
// { status: 'Service Unavailable' } → 503

Integrar com Uptime Kuma e alertas

Monitorar /health externamente com Uptime Kuma self-hosted:

bash
# No Uptime Kuma — adicionar monitor:
# Type: HTTP(s)
# URL: https://api.seudominio.com.br/health
# Interval: 60 seconds
# Keyword: "ok"          ← deve aparecer no JSON de resposta
# Expected Status Code: 200

# Configuração JSON esperada pelo Uptime Kuma:
# { "status": "ok", ... } → UP
# { "status": "degraded" } ou 503 → DOWN

# Para monitorar o banco separadamente:
# Type: HTTP(s)
# URL: https://api.seudominio.com.br/health/ready
# Expected Status Code: 200

# Status page pública — mostrar status dos serviços:
# Settings → Status Pages → New Status Page
# Adicionar os monitors da API, banco e Redis
# URL: status.seudominio.com.br

# Notificações: Telegram, Slack, email, webhook
# Settings → Notifications → Add Notification

Resposta estruturada para load balancers

Formato padronizado compatível com AWS, GCP e Kubernetes:

typescript
// Formato compatível com o padrão de health checks da maioria dos serviços:

interface HealthResponse {
  status: 'ok' | 'degraded' | 'error'
  version: string
  environment: string
  uptime: number
  timestamp: string
  checks: {
    [key: string]: {
      status: 'ok' | 'warning' | 'error'
      latencyMs?: number
      message?: string
    }
  }
}

app.get('/health', async (req, res) => {
  const response: HealthResponse = {
    status: 'ok',
    version: process.env.npm_package_version ?? 'unknown',
    environment: process.env.NODE_ENV ?? 'production',
    uptime: Math.round(process.uptime()),
    timestamp: new Date().toISOString(),
    checks: {},
  }

  // Não expor detalhes de erro em ambientes públicos:
  const isInternal = req.ip === '127.0.0.1' || req.headers['x-internal'] === process.env.INTERNAL_SECRET

  // ... executar verificações ...

  if (!isInternal) {
    // Simplificar resposta para requests externos:
    for (const key in response.checks) {
      delete response.checks[key].message
    }
  }

  res.status(response.status === 'ok' ? 200 : 503).json(response)
})

Não quer configurar manualmente?

Implante o VPS para APIs em menos de 5 minutos com a Runstack. Infraestrutura da OPEN Datacenter, com servidores em São Paulo.

Perguntas frequentes

Liveness: "o processo está vivo?", responde rápido, sem verificar dependências. Se falhar: Kubernetes reinicia o pod. Readiness: "está pronto para receber tráfego?", verifica banco, Redis, filas. Se falhar: Kubernetes para de enviar tráfego mas não reinicia. Para Docker e load balancers simples: um único /health que verifica dependências é suficiente.

Depende do uso. Se verificar o banco: qualquer lentidão no banco marca a API como unhealthy, o load balancer para de rotear e você tem uma cascata de falhas. Abordagem mais robusta: /health/live apenas verifica o processo (sem banco) para Kubernetes liveness; /health/ready verifica banco para readiness. Para Uptime Kuma: use /health/ready para detectar problemas reais.

Com cuidado. O endpoint em si deve ser público (sem autenticação) para load balancers e monitores externos. Mas a resposta detalhada (versão, erros, mensagens) pode revelar informações úteis para atacantes. Solução: /health retorna apenas { status: "ok" } publicamente; /health/detail com token interno retorna o JSON completo.

SELECT 1 é ultra-leve, não causa lock, não faz full scan, executa em < 1ms. Mas com 30 instâncias fazendo health check a cada 10s: 3 queries/seg ao banco. Use connection pool dedicado para health checks (pool de 1 conexão separado do pool principal) para não competir com as queries reais da aplicação.

Fly.io: sim, configura healthchecks no fly.toml com [[services.http_checks]]. Railway: aceita HEALTHCHECK no Dockerfile. Render: configurável no dashboard (Health Check Path). Em todos os casos: retornar 200 = saudável, qualquer outro código ou timeout = reiniciar. Configurar start_period/start-period de 15-30s para dar tempo ao processo inicializar antes do primeiro check.