---
title: "Health Check em Zig: Liveness, Readiness e /healthz em Produção"
url: "https://ziglang.com.br/artigos/zig-health-check-readiness-liveness/"
markdown_url: "https://ziglang.com.br/artigos/zig-health-check-readiness-liveness.MD"
description: "Como implementar health check em Zig: /healthz, /readyz, liveness vs readiness, dependências, shutdown, Kubernetes, systemd, Docker e checklist de produção."
date: "2026-08-12"
author: ""
---

# Health Check em Zig: Liveness, Readiness e /healthz em Produção

Como implementar health check em Zig: /healthz, /readyz, liveness vs readiness, dependências, shutdown, Kubernetes, systemd, Docker e checklist de produção.


**Health check em Zig** é o contrato operacional que diz a um orquestrador, load balancer ou script de deploy se o processo está vivo e se ele pode receber tráfego agora. Na prática: duas rotas baratas — `/healthz` (liveness) e `/readyz` (readiness) — com respostas previsíveis, dependências explícitas e integração com [graceful shutdown](/artigos/zig-graceful-shutdown-sigterm-sigint/).

Sem esse contrato, o deploy vira achismo. O Nginx continua mandando request para uma réplica que ainda não abriu a porta. O Kubernetes reinicia um pod só porque o banco falhou por dois segundos. O Docker marca o container como saudável enquanto a API já não aceita conexões. Zig não esconde isso atrás de um framework: você decide o que cada probe significa e o que cada status HTTP comunica.

Este artigo fecha o buraco entre menções espalhadas no [HTTP em produção](/artigos/zig-http-server-producao/), observabilidade, cloud native e systemd. O objetivo é um desenho reutilizável: o que medir, o que nunca medir, como modelar estado em Zig e como provar o comportamento com testes e smoke scripts.

## Resposta rápida

| Pergunta | Liveness (`/healthz`) | Readiness (`/readyz`) |
|---|---|---|
| O que responde? | o processo ainda reage? | posso receber tráfego agora? |
| Dependência externa? | não (ou só se for realmente o processo) | sim, se for obrigatória |
| Durante graceful shutdown | pode continuar `200` | deve virar `503` imediatamente |
| Falha típica | event loop travado, deadlock, OOM iminente | banco fora, fila obrigatória, config incompleta |
| Ação do orquestrador | reiniciar o processo | tirar do balanceador, sem matar ainda |
| Custo alvo | microsegundos a poucos ms | baixo, com timeout curto |
| Status saudável | `200` | `200` |
| Status ruim | `503` | `503` |

Se você está montando a API do zero, combine este guia com o [tutorial de HTTP server](/tutoriais/zig-http-server/), o checklist de [proxy reverso](/artigos/zig-nginx-proxy-reverso-load-balancing/) e o desenho de [API REST completa em Zig](/artigos/zig-api-rest-completa/).

## Por que dois endpoints e não um

A confusão mais comum é tratar "saúde" como um booleano único. Em produção existem pelo menos duas perguntas distintas:

1. **O processo está vivo?** Se a resposta for não, reiniciar pode ajudar.
2. **O processo está pronto para tráfego?** Se a resposta for não, tire a réplica do pool — mas não mate o binário se ele ainda está drenando trabalho útil.

Exemplos em que um único `/health` quebra o deploy:

- No shutdown, você devolve `503` para parar o tráfego. Se isso for liveness, o kubelet pode matar o pod antes do [drain de conexões](/artigos/zig-graceful-shutdown-sigterm-sigint/).
- O banco falha por 3 segundos. Se isso for liveness, o processo reinicia em cascata e piora a tempestade.
- O serviço sobe, abre a porta, mas ainda carrega cache quente. Sem readiness separado, o balanceador manda request cedo demais.

A regra operacional é simples: **liveness protege o processo; readiness protege o usuário**.

## O contrato mínimo de um health check em Zig

Todo health check saudável em Zig deve ser:

1. **Barato** — sem alocar buffers grandes, sem varrer disco, sem serializar relatórios enormes.
2. **Determinístico** — o mesmo estado interno deve produzir o mesmo status.
3. **Seguro** — sem segredos, sem stack trace completo, sem dados de cliente.
4. **Observável** — logs e métricas de falha de readiness, sem poluir cada probe bem-sucedido em alta frequência.
5. **Alinhado ao shutdown** — readiness cai no `SIGTERM` antes do exit.

Um esqueleto conceitual:

```zig
const std = @import("std");

pub const HealthState = struct {
    ready: std.atomic.Value(bool) = std.atomic.Value(bool).init(false),
    shutting_down: std.atomic.Value(bool) = std.atomic.Value(bool).init(false),
    // opcional: contadores, timestamp do último check de dependência, etc.
};

pub fn handleHealthz(state: *const HealthState) u16 {
    // Liveness: o handler só roda se o loop de HTTP ainda despacha trabalho.
    // Se você chegou aqui, o processo "respira".
    _ = state;
    return 200;
}

pub fn handleReadyz(state: *const HealthState, deps_ok: bool) u16 {
    if (state.shutting_down.load(.acquire)) return 503;
    if (!state.ready.load(.acquire)) return 503;
    if (!deps_ok) return 503;
    return 200;
}
```

O detalhe de `std.http.Server` muda entre versões do Zig; o contrato de estado compartilhado + status HTTP não muda. Confira a referência de `std.http.Server` e o tutorial de [servidor HTTP](/tutoriais/zig-http-server/) para o wiring da rota.

## Corpo da resposta: texto curto ou JSON pequeno

Para probes automáticos, o status HTTP basta. Ainda assim, um corpo mínimo ajuda humanos e scripts:

```http
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"status":"ok","role":"ready"}
```

Em falha:

```http
HTTP/1.1 503 Service Unavailable
Content-Type: application/json
Cache-Control: no-store

{"status":"not_ready","reason":"database"}
```

Boas práticas do corpo:

- `Cache-Control: no-store` — health check nunca deve ser cacheado por proxy intermediário;
- motivo curto e enumerável (`database`, `queue`, `config`, `shutdown`);
- sem PII, sem connection string, sem versão interna sensível;
- tamanho pequeno o bastante para caber em um log de uma linha.

Se o seu serviço já emite métricas Prometheus, um contador `readyz_failures_total{reason="database"}` vale mais do que um JSON verboso. Veja [observabilidade com logs e Prometheus](/artigos/zig-observabilidade-logs-prometheus/) e OpenTelemetry em Zig.

## O que colocar em liveness

Liveness deve responder: **o processo ainda é capaz de progresso útil?**

Inclua:

- o fato de o handler HTTP ter sido despachado;
- opcionalmente, um heartbeat de uma thread de trabalho crítica (atualizado com relógio monotônico);
- opcionalmente, um limite de memória se o processo já está em estado irrecuperável.

Não inclua:

- ping em banco de dados;
- chamada a API externa;
- leitura de fila remota;
- verificação de disco de alta latência;
- qualquer I/O que possa bloquear o worker do probe.

Se o event loop travar de verdade, o probe deixa de responder e o orquestrador age. Esse é o sinal mais honesto de liveness em um servidor HTTP.

## O que colocar em readiness

Readiness deve responder: **se eu mandar tráfego real agora, este processo consegue cumprir o contrato da API?**

Dependências típicas:

| Dependência | Como checar com segurança | Timeout sugerido |
|---|---|---|
| PostgreSQL / MySQL | `SELECT 1` ou ping do pool | 50–200 ms |
| Redis | `PING` | 30–100 ms |
| Fila obrigatória | conexão + auth, sem consumir job | 50–200 ms |
| Arquivo de config / secret | existência + parse já feito no boot | local, quase zero |
| Feature flag crítica | estado em memória já carregado | local |
| Upstream obrigatório | só se a API for um proxy puro | curto e com circuit breaker |

Regras práticas:

1. **Só dependências obrigatórias.** Se a API degrada sem cache, o cache não deve derrubar readiness.
2. **Timeout curto e explícito.** Readiness lento vira indisponibilidade artificial.
3. **Cacheie o resultado por 1–5 segundos** se o volume de probes for alto, com invalidação no shutdown.
4. **Nunca bloqueie o event loop inteiro** em um check de dependência sem limite.

Para pools e queries, use o guia de [banco de dados em Zig](/artigos/zig-banco-dados-integracoes/) e o de [PostgreSQL com libpq](/artigos/zig-postgresql-libpq-producao/). Para resiliência de cliente, o [circuit breaker com timeout e retry](/artigos/zig-circuit-breaker-timeout-retry/) evita que o próprio readiness amplifique uma pane.

## Integração com graceful shutdown

O casamento correto entre health e shutdown é o que separa deploy limpo de 502 em cascata:

1. Processo sobe, carrega config, conecta dependências.
2. Marca `ready = true` só quando puder servir.
3. Orquestrador começa a mandar tráfego.
4. Chega `SIGTERM` / `SIGINT`.
5. Marca `shutting_down = true` e `ready = false` **antes** de fechar o listener.
6. `/readyz` passa a responder `503`; balanceador remove a réplica.
7. Drain de HTTP e workers com timeout.
8. Exit com código previsível.

Esse fluxo está detalhado no artigo de [graceful shutdown com SIGTERM e SIGINT](/artigos/zig-graceful-shutdown-sigterm-sigint/). O erro clássico é o inverso: fechar o socket primeiro e só depois lembrar do readiness — o probe e o balanceador veem falha de conexão em vez de um `503` educado.

## Kubernetes, Docker e systemd

### Kubernetes

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

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

Ajuste `initialDelaySeconds` ao tempo real de boot do binário Zig (quase sempre baixo) e ao tempo de aquecimento de dependências. Para o desenho de deploy em cluster, veja Zig cloud native e [Kubernetes operators em Zig](/artigos/zig-kubernetes-operators/).

### Docker

```yaml
healthcheck:
  test: ["CMD", "wget", "--spider", "-q", "http://127.0.0.1:8080/healthz"]
  interval: 10s
  timeout: 2s
  retries: 3
  start_period: 5s
```

Docker Compose costuma ter um único `healthcheck`. Se você só puder expor um, prefira **liveness** no healthcheck do engine e deixe readiness com o load balancer / orchestrator de cima. No código, um modo `--health` de CLI que sai `0/1` também funciona bem para containers sem HTTP — padrão citado no guia de Docker com Zig.

### systemd

Com systemd, o socket ou o serviço pode usar:

```bash
curl -fsS http://127.0.0.1:8080/healthz
curl -fsS http://127.0.0.1:8080/readyz
```

em `ExecStartPost`, scripts de deploy e checagens de `systemctl is-active`. O artigo de serviço systemd em produção mostra o encaixe com logs e restart policy.

### Nginx / load balancer

O proxy na frente deve usar a rota de readiness (ou um `location` interno equivalente) para decidir se a réplica entra no upstream. O guia de [Nginx, proxy reverso e load balancing](/artigos/zig-nginx-proxy-reverso-load-balancing/) cobre `max_fails`, `fail_timeout` e a janela perigosa de deploy com várias réplicas.

## Implementação de rotas no servidor HTTP

Independentemente da API exata de `std.http.Server` na sua versão do Zig, o desenho de roteamento costuma ser:

```zig
// Pseudocódigo de despacho
if (method == .GET and pathEql(path, "/healthz")) {
    // 200 quase sempre; corpo mínimo
    return writeStatus(response, 200, "{\"status\":\"ok\",\"role\":\"live\"}");
}

if (method == .GET and pathEql(path, "/readyz")) {
    const code = handleReadyz(&app.state, checkDeps(&app));
    if (code == 200) {
        return writeStatus(response, 200, "{\"status\":\"ok\",\"role\":\"ready\"}");
    }
    return writeStatus(response, 503, "{\"status\":\"not_ready\",\"reason\":\"deps\"}");
}
```

Detalhes que evitam dor:

- aceite só `GET` (e, se quiser, `HEAD`);
- não conte health check na métrica de "request de negócio" sem label separado;
- não aplique [rate limiting](/artigos/zig-rate-limiting-token-bucket/) agressivo nas rotas de probe da rede interna;
- não exija [JWT](/artigos/zig-jwt-autenticacao-api/) no probe local;
- se o servidor estiver atrás de mTLS de serviço, configure o probe do orquestrador na mesma rede/confiança.

## Dependências opcionais vs obrigatórias

Nem toda falha externa deve derrubar readiness. Classifique no boot:

```text
obrigatória  -> readiness falha
opcional     -> métrica + log + degradação da feature
```

Exemplos:

- API de pagamentos em um checkout: se for o único meio de pagamento, é obrigatória; se houver fallback offline, talvez não.
- Cache Redis de sessão: se a API consegue cair para store primário, readiness pode permanecer OK.
- Worker de e-mail: o processo HTTP de leitura não precisa falhar readiness porque o SMTP caiu.

Essa classificação também melhora feature flags e rollout: você pode marcar uma dependência nova como opcional até o percentual de tráfego justificar o contrário.

## Testes que realmente pegam regressão

Health check sem teste vira documentação. Cubra pelo menos:

1. **Boot frio** — antes de `ready=true`, `/readyz` é `503` e `/healthz` é `200`.
2. **Pronto** — com dependências mockadas OK, ambos `200`.
3. **Dependência ruim** — readiness `503` com `reason` estável; liveness intacto.
4. **Shutdown** — após sinal (ou flag injetada), readiness cai; liveness permanece coerente até o exit.
5. **Timeout de dependência** — se o mock dorme demais, readiness falha por timeout, não trava o teste.
6. **Smoke de processo** — sobe o binário em porta efêmera, faz `curl` real, encerra.

Um smoke mínimo:

```bash
./bin/api --port 18080 &
pid=$!
trap 'kill -TERM $pid' EXIT

for i in 1 2 3 4 5 6 7 8 9 10; do
  if curl -fsS "http://127.0.0.1:18080/healthz" >/dev/null; then
    break
  fi
  sleep 0.2
done

curl -fsS "http://127.0.0.1:18080/healthz" | grep -q ok
# readiness pode exigir fixtures de dependência no ambiente de CI
```

Para a cultura de testes em Zig, veja o [guia completo de testes](/artigos/zig-testes-guia-completo/) e testes baseados em propriedade.

## Erros comuns (e como evitar)

**Um único endpoint para liveness e readiness.** Já coberto: separa as perguntas.

**Readiness que faz trabalho pesado.** Se o check precisa varrer tabela inteira, você criou uma DoS self-service a cada 5 segundos.

**Liveness que depende de rede.** Reinício em cascata quando a rede treme.

**Sem `Cache-Control: no-store`.** Proxy intermediário devolve health antigo e esconde pane real.

**Sem alinhar ao shutdown.** Balanceador ainda manda request enquanto o processo já não quer tráfego.

**Corpo com segredo.** Connection string, token e path interno não pertencem à resposta de probe.

**Probe na porta pública sem filtro.** Em edge exposta, restrinja por rede ou coloque o probe só no listener interno.

**Métricas sem label de rota.** Health check alto volume distorce latência p99 da API de negócio.

**Ignorar `HEAD`.** Alguns load balancers preferem `HEAD`; se você rejeita, documenta ou trata igual ao `GET` sem body.

## Checklist de produção

- [ ] `/healthz` e `/readyz` existem e respondem rápido.
- [ ] Liveness não consulta dependência externa cara.
- [ ] Readiness só falha por dependência **obrigatória** ou shutdown.
- [ ] No `SIGTERM`, readiness vira `503` antes do drain completo.
- [ ] Respostas usam `200` / `503` de forma estável.
- [ ] `Cache-Control: no-store` (ou equivalente) está presente.
- [ ] Probes do Kubernetes/Docker/Nginx apontam para as rotas certas.
- [ ] Timeouts de dependência são curtos e testados.
- [ ] Métricas e logs de falha de readiness existem sem flood no sucesso.
- [ ] Smoke test de processo cobre health no CI ou no pipeline de deploy.
- [ ] Rede de probe não exige auth de usuário final.
- [ ] Documentação interna lista o que cada `reason` significa.

## Quando health check sozinho não basta

Health check responde "agora". Ele não substitui:

- [rate limiting](/artigos/zig-rate-limiting-token-bucket/) e proteção de borda;
- [circuit breaker](/artigos/zig-circuit-breaker-timeout-retry/) em clientes;
- [TLS e mTLS](/artigos/zig-tls-https-certificados-mtls/) quando o tráfego exige;
- testes de carga e benchmark.

Pense no health check como o semáforo do cruzamento: necessário, barato, inequívoco. O resto do sistema ainda precisa frear, sinalizar e recuperar.

## FAQ na prática

### Devo falhar readiness se a latência p99 subiu?

Em geral, não. Latência alta é sinal de capacidade ou dependência lenta; trate com autoscaling, rate limit e circuit breaker. Readiness deve falhar quando o serviço **não pode** cumprir o contrato, não quando está apenas mais lento que o ideal.

### E se eu tiver vários listeners (admin vs público)?

Exponha probes no listener interno/admin. O listener público continua servindo a API. Isso reduz superfície de ataque e evita que um WAF externo interfira no kubelet.

### Preciso de `/startup` além de liveness/readiness?

Em boot longo (raro em Zig puro, comum se você carrega modelos ou migra estado), um startup probe separado evita que o liveness mate o processo ainda inicializando. Se o boot é rápido, readiness com `ready=false` no começo já resolve.

### Health check de worker sem HTTP?

Use um comando `--health` no mesmo binário, ou um arquivo heartbeat em disco/tmpfs atualizado pelo loop principal, lido por um sidecar. O importante é o mesmo contrato mental: vivo vs pronto.

## Próximos passos

1. Adicione `/healthz` e `/readyz` ao servidor com estado atômico compartilhado.
2. Ligue readiness ao fluxo de [graceful shutdown](/artigos/zig-graceful-shutdown-sigterm-sigint/).
3. Configure probes no Docker, systemd ou Kubernetes.
4. Emita métricas de falha e encaixe no pipeline de observabilidade.
5. Só então refine dependências opcionais, cache de check e dashboards.

Zig recompensa esse estilo incremental: cada decisão fica no código, cada probe vira contrato testável, e o deploy deixa de depender de "parece que subiu". Com health check bem desenhado, o orquestrador para de adivinhar — e o seu binário passa a falar, em HTTP claro, se está vivo e se está pronto.
