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.
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, 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, o checklist de proxy reverso e o desenho de API REST completa em Zig.
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:
- O processo está vivo? Se a resposta for não, reiniciar pode ajudar.
- 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
503para parar o tráfego. Se isso for liveness, o kubelet pode matar o pod antes do drain de conexões. - 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:
- Barato — sem alocar buffers grandes, sem varrer disco, sem serializar relatórios enormes.
- Determinístico — o mesmo estado interno deve produzir o mesmo status.
- Seguro — sem segredos, sem stack trace completo, sem dados de cliente.
- Observável — logs e métricas de falha de readiness, sem poluir cada probe bem-sucedido em alta frequência.
- Alinhado ao shutdown — readiness cai no
SIGTERMantes do exit.
Um esqueleto conceitual:
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 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/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
X-Content-Type-Options: nosniff
{"status":"ok","role":"ready"}
Em falha:
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 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:
- Só dependências obrigatórias. Se a API degrada sem cache, o cache não deve derrubar readiness.
- Timeout curto e explícito. Readiness lento vira indisponibilidade artificial.
- Cacheie o resultado por 1–5 segundos se o volume de probes for alto, com invalidação no shutdown.
- 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 e o de PostgreSQL com libpq. Para resiliência de cliente, o circuit breaker com timeout e 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:
- Processo sobe, carrega config, conecta dependências.
- Marca
ready = truesó quando puder servir. - Orquestrador começa a mandar tráfego.
- Chega
SIGTERM/SIGINT. - Marca
shutting_down = trueeready = falseantes de fechar o listener. /readyzpassa a responder503; balanceador remove a réplica.- Drain de HTTP e workers com timeout.
- Exit com código previsível.
Esse fluxo está detalhado no artigo de graceful shutdown com SIGTERM e 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
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.
Docker
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:
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 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:
// 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 agressivo nas rotas de probe da rede interna;
- não exija JWT 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:
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:
- Boot frio — antes de
ready=true,/readyzé503e/healthzé200. - Pronto — com dependências mockadas OK, ambos
200. - Dependência ruim — readiness
503comreasonestável; liveness intacto. - Shutdown — após sinal (ou flag injetada), readiness cai; liveness permanece coerente até o exit.
- Timeout de dependência — se o mock dorme demais, readiness falha por timeout, não trava o teste.
- Smoke de processo — sobe o binário em porta efêmera, faz
curlreal, encerra.
Um smoke mínimo:
./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 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
-
/healthze/readyzexistem 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 vira503antes do drain completo. - Respostas usam
200/503de 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
reasonsignifica.
Quando health check sozinho não basta
Health check responde “agora”. Ele não substitui:
- rate limiting e proteção de borda;
- circuit breaker em clientes;
- TLS e 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
- Adicione
/healthze/readyzao servidor com estado atômico compartilhado. - Ligue readiness ao fluxo de graceful shutdown.
- Configure probes no Docker, systemd ou Kubernetes.
- Emita métricas de falha e encaixe no pipeline de observabilidade.
- 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.