---
title: "Health Checks em Go: Liveness, Readiness e Startup"
url: "https://golang.com.br/blog/health-checks-go-liveness-readiness-startup/"
markdown_url: "https://golang.com.br/blog/health-checks-go-liveness-readiness-startup.MD"
description: "Aprenda health checks em Go para APIs e workers: liveness, readiness, startup, dependências, Kubernetes, timeouts, JSON, métricas e deploy seguro."
date: "2026-06-11"
author: "Golang Brasil"
---

# Health Checks em Go: Liveness, Readiness e Startup

Aprenda health checks em Go para APIs e workers: liveness, readiness, startup, dependências, Kubernetes, timeouts, JSON, métricas e deploy seguro.


Health checks em Go parecem simples demais para merecer um guia: criar uma rota `/healthz`, devolver `200` e seguir em frente. Em produção, essa simplificação causa incidentes discretos. Um serviço pode estar vivo, mas incapaz de receber tráfego. Pode aceitar requisições antes de carregar configuração. Pode derrubar todos os pods porque o banco ficou lento por alguns segundos. Pode esconder dependência quebrada até o checkout, o login ou o worker começar a falhar para usuários reais.

Go ajuda porque a biblioteca padrão já oferece `net/http`, `context`, timeouts e concorrência leve. O desafio não é escrever o handler; é decidir o que cada check significa. `liveness`, `readiness` e `startup` respondem perguntas diferentes. Misturar essas perguntas em uma rota única deixa Kubernetes, Docker, balanceadores e operadores humanos sem sinal confiável.

Este guia mostra como desenhar health checks em serviços Go para APIs e workers: endpoints separados, checagem de dependências, timeouts curtos, JSON de diagnóstico, integração com Kubernetes, cuidados com banco, métricas e erros comuns. Ele complementa [graceful shutdown em Go](/blog/graceful-shutdown-go-producao/), [OpenTelemetry em Go](/blog/go-opentelemetry-observabilidade-tracing-metricas/), [Go com Kubernetes](/tutoriais/go-kubernetes/), [pgxpool em produção](/blog/pgxpool-go-postgresql-producao/) e [context, timeout e cancelamento](/blog/context-timeout-cancelamento-go/).

## O que cada health check deve responder

A primeira regra é separar semântica. Um health check não é uma página de status completa. Ele é um contrato operacional entre o processo e a plataforma que decide se aquela instância deve iniciar, continuar viva ou receber tráfego.

`liveness` responde: o processo está vivo o suficiente para tentar se recuperar sem restart? Em Go, normalmente isso significa que o servidor HTTP responde e o runtime não travou. Liveness não deve depender de PostgreSQL, Redis, S3, API externa ou fila. Se o banco cai por 30 segundos e todos os pods reiniciam ao mesmo tempo, você cria um segundo incidente: tempestade de restart, perda de cache local, aquecimento de conexão e mais pressão na dependência já instável.

`readiness` responde: esta instância deve receber tráfego novo agora? Aqui faz sentido considerar dependências críticas. Se a API precisa de banco para praticamente toda rota, readiness pode falhar quando o pool não consegue pingar dentro de um timeout curto. Se Redis é apenas cache opcional, readiness não deve falhar por causa dele. Se um fornecedor externo está fora, talvez a API ainda possa aceitar pedidos e enfileirar retry.

`startup` responde: o processo terminou a inicialização? Esse check é útil quando a aplicação precisa carregar modelos, migrar cache, abrir conexões, aquecer rotas ou executar validações iniciais. Sem startup probe, Kubernetes pode matar um pod lento antes de ele ficar pronto. Com startup probe, você dá uma janela maior para a primeira inicialização sem enfraquecer liveness depois.

## Endpoints básicos com net/http

Um desenho enxuto começa com estado explícito de readiness. Durante o shutdown, você marca a instância como não pronta antes de encerrar o servidor. Durante startup, só marca pronta depois que configuração, conexões e validações mínimas passaram.

```go
package health

import (
    "encoding/json"
    "net/http"
    "sync/atomic"
    "time"
)

type Handler struct {
    ready atomic.Bool
    start time.Time
}

func NewHandler() *Handler {
    h := &Handler{start: time.Now()}
    return h
}

func (h *Handler) SetReady(v bool) {
    h.ready.Store(v)
}

func (h *Handler) Live(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusOK)
    _, _ = w.Write([]byte("ok\n"))
}

func (h *Handler) Ready(w http.ResponseWriter, r *http.Request) {
    if !h.ready.Load() {
        http.Error(w, "not ready", http.StatusServiceUnavailable)
        return
    }

    writeJSON(w, http.StatusOK, map[string]any{
        "status": "ok",
        "uptime_seconds": int(time.Since(h.start).Seconds()),
    })
}

func writeJSON(w http.ResponseWriter, code int, payload any) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(code)
    _ = json.NewEncoder(w).Encode(payload)
}
```

No `main`, registre rotas simples e sem autenticação, mas não exponha informação sensível:

```go
mux := http.NewServeMux()
health := health.NewHandler()

mux.HandleFunc("GET /livez", health.Live)
mux.HandleFunc("GET /readyz", health.Ready)

// Depois de carregar config, abrir pool e validar dependências críticas:
health.SetReady(true)
```

Essa versão ainda não checa dependências, mas já evita dois erros comuns: liveness acoplada ao banco e readiness que nunca muda durante shutdown.

## Checando dependências com timeout curto

Readiness pode consultar dependências críticas, mas sempre com orçamento pequeno. Health check não pode virar uma requisição lenta que consome goroutines e conexões em massa. Se o balanceador chama `/readyz` a cada poucos segundos em dezenas de pods, cada check precisa ser barato.

Um padrão simples é definir uma interface pequena:

```go
type Checker interface {
    Name() string
    Check(ctx context.Context) error
}
```

Para PostgreSQL com `pgxpool`, o checker pode usar `Ping` com contexto:

```go
package health

import (
    "context"
    "fmt"

    "github.com/jackc/pgx/v5/pgxpool"
)

type PostgresChecker struct {
    Pool *pgxpool.Pool
}

func (p PostgresChecker) Name() string { return "postgres" }

func (p PostgresChecker) Check(ctx context.Context) error {
    if p.Pool == nil {
        return fmt.Errorf("pool nil")
    }
    return p.Pool.Ping(ctx)
}
```

O handler de readiness agrega checks com timeout:

```go
type Handler struct {
    ready   atomic.Bool
    start   time.Time
    checks  []Checker
    timeout time.Duration
}

func NewHandler(checks ...Checker) *Handler {
    return &Handler{
        start: time.Now(),
        checks: checks,
        timeout: 750 * time.Millisecond,
    }
}

func (h *Handler) Ready(w http.ResponseWriter, r *http.Request) {
    if !h.ready.Load() {
        writeJSON(w, http.StatusServiceUnavailable, map[string]any{
            "status": "not_ready",
            "reason": "draining_or_starting",
        })
        return
    }

    ctx, cancel := context.WithTimeout(r.Context(), h.timeout)
    defer cancel()

    details := map[string]string{}
    ok := true

    for _, check := range h.checks {
        if err := check.Check(ctx); err != nil {
            ok = false
            details[check.Name()] = err.Error()
            continue
        }
        details[check.Name()] = "ok"
    }

    code := http.StatusOK
    status := "ok"
    if !ok {
        code = http.StatusServiceUnavailable
        status = "not_ready"
    }

    writeJSON(w, code, map[string]any{
        "status": status,
        "checks": details,
    })
}
```

Esse desenho é bom o bastante para muitos serviços. Ele também força a pergunta certa: quais dependências entram em readiness? Uma API de catálogo pode precisar de PostgreSQL, mas não de serviço de e-mail. Um endpoint de upload pode precisar de storage privado, mas não de motor de thumbnail. Um worker de cobrança pode depender da fila e do banco, mas talvez não de uma API externa que tem retry.

## Kubernetes: liveness, readiness e startup probes

No Kubernetes, a configuração deve refletir a semântica dos endpoints. Exemplo para uma API HTTP:

```yaml
livenessProbe:
  httpGet:
    path: /livez
    port: 8080
  initialDelaySeconds: 10
  periodSeconds: 10
  timeoutSeconds: 1
  failureThreshold: 3

readinessProbe:
  httpGet:
    path: /readyz
    port: 8080
  periodSeconds: 5
  timeoutSeconds: 1
  failureThreshold: 2

startupProbe:
  httpGet:
    path: /livez
    port: 8080
  periodSeconds: 2
  timeoutSeconds: 1
  failureThreshold: 30
```

A `startupProbe` acima dá até 60 segundos para o processo inicializar. Depois que ela passa, liveness assume o papel normal. Ajuste números conforme a aplicação. Uma API Go simples não deveria precisar de muito tempo. Um serviço que carrega cache grande ou valida muitos recursos pode precisar de mais margem.

Evite `initialDelaySeconds` gigante para esconder startup lento. Ele atrasa detecção real de problemas. Se a primeira inicialização é especial, use startup probe. Se toda inicialização é lenta sem motivo, corrija o startup.

## Readiness durante graceful shutdown

Health check e shutdown precisam conversar. Quando o processo recebe `SIGTERM`, a sequência segura é:

1. Marcar readiness como falsa.
2. Esperar uma pequena margem para o balanceador parar de enviar tráfego novo.
3. Chamar `http.Server.Shutdown` com timeout menor que o prazo da plataforma.
4. Fechar workers, filas e conexões.
5. Sair do processo.

Em código, a parte crítica é mudar o estado antes do shutdown:

```go
sigctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT)
defer stop()

<-sigctx.Done()
health.SetReady(false)

time.Sleep(5 * time.Second) // margem para endpoints/proxies atualizarem

ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()

if err := server.Shutdown(ctx); err != nil {
    slog.Error("shutdown failed", "error", err)
}
```

Esse padrão reduz requisições cortadas no meio de deploy. Ele é especialmente importante quando há keep-alive, proxies, ingress controllers ou filas de requisições. O artigo de [graceful shutdown em Go](/blog/graceful-shutdown-go-producao/) aprofunda essa sequência.

## O que não colocar no health check

Não coloque lógica cara. Health check não deve rodar consulta complexa, contar linhas, chamar API de pagamento, validar token externo, processar fila ou testar envio de e-mail. Isso cria ruído, custo e dependência circular: o sistema de monitoramento passa a causar carga relevante no sistema monitorado.

Também não vaze segredo. O JSON pode dizer que `postgres` falhou, mas não precisa imprimir DSN, usuário, host interno completo, query, payload ou stack trace. Em ambiente público, considere limitar o detalhe ou expor checks detalhados apenas em rede interna.

Não faça readiness falhar por dependência opcional. Se Redis é cache e a aplicação consegue operar com miss, registre a falha em métrica e log, mas mantenha a instância pronta. Se Redis guarda sessão ou lock crítico, aí sim pode ser parte da readiness.

Não use liveness como detector de bug de negócio. Se uma rota específica falha por validação, migration ruim ou contrato externo quebrado, reiniciar o processo raramente resolve. Liveness deve ser conservadora: restart só ajuda quando o processo travou, deadlockou, vazou recurso irrecuperável ou entrou em estado impossível de recuperar.

## Observabilidade dos próprios checks

Health checks são sinais operacionais, mas não substituem métricas. Registre duração e resultado por checker com cuidado de cardinalidade. Labels como `check="postgres"` e `status="ok"` são aceitáveis. Não coloque erro bruto como label.

Logs também devem ser econômicos. Se o banco cai e `/readyz` é chamado a cada 5 segundos por 20 pods, logar erro em todo request cria barulho. Prefira métricas para estado contínuo e logs em transições importantes: ficou não pronto, voltou a pronto, iniciou shutdown.

Para tracing, normalmente não vale criar spans para cada health check de liveness. Eles poluem amostragem. Se você instrumenta readiness, filtre ou marque como rota operacional. O objetivo é melhorar diagnóstico, não transformar `/readyz` na rota mais visível do sistema.

## Health checks para workers Go

Workers sem HTTP também precisam de saúde operacional. Uma opção simples é expor um servidor HTTP pequeno ao lado do worker apenas para `/livez` e `/readyz`. Outra opção é publicar heartbeat em métrica, mas o endpoint costuma integrar melhor com Kubernetes.

Readiness de worker significa: ele deve receber trabalho novo? Durante shutdown, marque não pronto e pare de buscar mensagens novas. Termine ou devolva o trabalho em andamento conforme a semântica da fila. Para SQS, por exemplo, talvez seja melhor não deletar a mensagem se o processamento não concluiu. Para Kafka, controle commit de offset. Para jobs locais, feche canais de entrada e aguarde workers.

Liveness de worker continua simples: processo vivo, loop principal não travado e servidor operacional respondendo. Se a fila está vazia, isso não é falha. Se a dependência externa está fora, talvez o worker deva ficar vivo e fazer backoff, não reiniciar em loop.

## Checklist de produção

Antes de publicar um serviço Go, revise:

- `/livez` não depende de banco, cache ou API externa.
- `/readyz` falha apenas para dependências críticas e com timeout curto.
- Startup lento usa `startupProbe`, não liveness frágil.
- Shutdown marca readiness falsa antes de chamar `Shutdown`.
- Erros retornados não vazam segredo.
- Checks têm métricas de duração e status.
- Dependências opcionais não derrubam readiness.
- Timeouts dos probes são menores que o orçamento do handler.
- O deploy foi testado com `kubectl rollout restart` ou ambiente equivalente.

Health checks bons são chatos no melhor sentido. Eles não tentam provar que todo o produto funciona. Eles dão à plataforma um sinal claro para iniciar, manter, remover tráfego e encerrar instâncias. Em Go, essa clareza combina com o estilo da linguagem: handlers pequenos, contexto explícito, dependências injetadas e comportamento previsível durante deploy.

Se você está montando um projeto para portfólio ou entrevista, adicionar `/livez`, `/readyz`, shutdown gracioso, logs estruturados e um README explicando esses trade-offs mostra maturidade real de produção. Muitas [vagas Go no Brasil](/vagas/) pedem Docker, Kubernetes, observabilidade e ownership; health checks conectam esses temas em uma prática simples de demonstrar. Para comparar como esses sinais aparecem em descrições de vagas de outras stacks, acompanhe também o portal <a href="https://eu.dev.br/vagas/" target="_blank" rel="noopener noreferrer" onclick="umami.track('portfolio-site-click', { destination: 'eu.dev.br' })">eu.dev.br</a>.
