---
title: "Graceful Shutdown em Zig com SIGTERM, SIGINT e Drain de Conexões"
url: "https://ziglang.com.br/artigos/zig-graceful-shutdown-sigterm-sigint/"
markdown_url: "https://ziglang.com.br/artigos/zig-graceful-shutdown-sigterm-sigint.MD"
description: "Como implementar graceful shutdown em Zig: SIGTERM e SIGINT, flag atômica, drain de HTTP e workers, timeout de encerramento, systemd e checklist de produção."
date: "2026-08-11"
author: ""
---

# Graceful Shutdown em Zig com SIGTERM, SIGINT e Drain de Conexões

Como implementar graceful shutdown em Zig: SIGTERM e SIGINT, flag atômica, drain de HTTP e workers, timeout de encerramento, systemd e checklist de produção.


**Graceful shutdown em Zig** é o contrato operacional de receber `SIGTERM` ou `SIGINT`, parar de aceitar trabalho novo, esvaziar (ou cancelar com limite) o que já está em andamento e sair com estado previsível. Na prática: uma flag atômica no handler, um loop principal que reage a ela, um timeout de drain e limpeza explícita de sockets, workers, arquivos e métricas.

Esse padrão aparece em todo serviço real — API HTTP, worker de fila, agente local, CLI de longa duração e daemon atrás de systemd. Sem ele, o deploy vira roleta: resposta pela metade, job duplicado, arquivo temporário órfão e log sem causa. Com ele, o stop do orquestrador deixa de ser um acidente e passa a ser um caminho de código testável.

A receita de sinais em Zig cobre a captura básica. Este artigo fecha o ciclo de produção: o que o handler pode fazer, como drenar HTTP e workers, como alinhar timeout com systemd/Docker e como provar o comportamento com testes.

## Resposta rápida

| Momento | Ação correta em Zig |
|---|---|
| `SIGINT` / `SIGTERM` chega | gravar flag atômica; **não** fazer I/O no handler |
| depois do sinal | parar `accept` / consumo de fila; manter conexões ativas |
| trabalho em andamento | terminar se for curto; cancelar ou reenfileirar se for longo |
| recursos | fechar listener, flush de logs, liberar locks e arquivos |
| timeout de drain | registrar pendências e sair; não bloquear para sempre |
| sucesso | `exit 0` (ou código combinado com o supervisor) |
| falha de limpeza | logar e sair com código ≠ 0 para o restart policy agir |

Se você está montando o servidor do zero, combine este guia com o [tutorial de HTTP server](/tutoriais/zig-http-server/) e o checklist de [HTTP em produção](/artigos/zig-http-server-producao/). Para jobs assíncronos, leia também filas e workers em background.

## Por que “matar o processo” não basta

Em desenvolvimento, `Ctrl+C` e o terminal some. Em produção, o mesmo binário precisa sobreviver a:

- `systemctl stop` e `systemctl restart`;
- rolling update em container com `stop_grace_period`;
- balanceador que remove a réplica do pool e depois manda `SIGTERM`;
- deploy que sobe a nova versão antes de a antiga terminar;
- jobs que escrevem em disco, chamam API externa ou atualizam cache.

Se o processo morre no meio de uma resposta HTTP, o cliente pode reenviar e gerar efeito colateral. Se um worker some no meio de um job não idempotente, você pode cobrar duas vezes, enviar dois e-mails ou deixar um arquivo pela metade. Graceful shutdown não elimina esses riscos sozinho — ele cria a janela para o programa **decidir** o que fazer com o trabalho residual.

Zig favorece esse desenho porque não esconde o processo atrás de um runtime com hooks mágicos. Você escreve o contrato: quais sinais importam, qual flag muda, quem consulta a flag e quanto tempo o drain pode durar.

## O contrato mínimo em quatro etapas

Todo graceful shutdown saudável segue a mesma ordem:

1. **Sinal** — o SO notifica o processo.
2. **Marca** — o handler grava um estado compartilhado seguro.
3. **Drain** — o loop principal para de aceitar trabalho novo e conclui o restante com limite de tempo.
4. **Saída** — recursos liberados, logs finais, código de saída previsível.

Inverter a ordem é o erro clássico: tentar fechar sockets dentro do handler, logar com alocação, ou esperar “todo mundo terminar” sem timeout. O supervisor (systemd, Docker, Kubernetes) **sempre** tem um prazo. Se o seu programa não tiver o mesmo prazo, o kernel manda `SIGKILL` e o graceful vira kill.

## Signal handler seguro: só a flag

Em POSIX, o conjunto de funções assíncrono-seguras dentro de um handler é pequeno. Alocar com o allocator do heap, formatar string, escrever em arquivo ou chamar a maior parte da stdlib **não** é seguro. O padrão portátil em Zig é:

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

var shutting_down = std.atomic.Value(bool).init(false);

fn onSignal(sig: c_int) callconv(.C) void {
    _ = sig;
    shutting_down.store(true, .seq_cst);
}

fn installShutdownHandlers() !void {
    const act = posix.Sigaction{
        .handler = .{ .handler = onSignal },
        .mask = posix.empty_sigset,
        .flags = 0,
    };
    try posix.sigaction(posix.SIG.INT, &act, null);
    try posix.sigaction(posix.SIG.TERM, &act, null);
}
```

Pontos de design:

- **um handler, dois sinais** — `SIGINT` e `SIGTERM` pedem o mesmo fluxo;
- **`std.atomic.Value(bool)`** — leitura/escrita sem data race entre o handler e as threads;
- **sem log no handler** — o loop principal loga `service_stop reason=signal` quando vê a flag;
- **sem `std.process.exit` no handler** — sair ali pula o drain.

Se você precisa de `SIGHUP` para reload de configuração, use **outra** flag. Misturar reload e stop no mesmo booleano gera restart confuso e perda de trabalho.

## Loop principal que reage ao sinal

O handler só avisa. Quem age é o loop:

```zig
pub fn main() !void {
    try installShutdownHandlers();

    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    var app = try App.init(allocator);
    defer app.deinit();

    const drain_ns: u64 = 15 * std.time.ns_per_s;
    var drain_deadline: ?i128 = null;

    while (true) {
        if (shutting_down.load(.seq_cst)) {
            if (drain_deadline == null) {
                std.log.info("service_stop reason=signal starting_drain", .{});
                try app.stopAccepting();
                drain_deadline = std.time.nanoTimestamp() + drain_ns;
            }

            if (app.activeWork() == 0) break;

            const now = std.time.nanoTimestamp();
            if (now >= drain_deadline.?) {
                std.log.warn("drain_timeout active_work={d}", .{app.activeWork()});
                try app.forceCancel();
                break;
            }
        }

        try app.pollOnce();
    }

    std.log.info("service_exit code=0", .{});
}
```

Esse esqueleto serve para HTTP, TCP genérico e workers. A API de `App` pode ser a que o seu projeto já tem: o importante é separar **aceitar trabalho novo** de **trabalho em voo**.

## Drain de servidor HTTP

Para um servidor HTTP, graceful shutdown significa:

1. parar de aceitar conexões novas no listener;
2. continuar lendo e respondendo requisições já aceitas;
3. fechar conexões ociosas;
4. cancelar com critério as que ainda não terminaram quando o timeout estoura;
5. só então fechar o socket de escuta de forma definitiva e sair.

Na borda, o reverse proxy (Nginx, Caddy, Traefik) e o balanceador devem parar de enviar tráfego **antes** ou **junto** com o `SIGTERM`. Se o proxy ainda manda requests enquanto o binário já recusa `accept`, o cliente vê erro que parece falha da aplicação. Coordene:

- health check que passa a falhar assim que a flag sobe (`/ready` → 503);
- liveness separado de readiness;
- timeout de stop do supervisor ≥ timeout de drain do processo.

Um endpoint de readiness simples:

```zig
fn handleReady(shutting_down_flag: *const std.atomic.Value(bool), response: anytype) !void {
    if (shutting_down_flag.load(.seq_cst)) {
        // 503: tire a réplica do balanceador, mas deixe liveness OK se o processo ainda respira
        try response.status(.service_unavailable);
        try response.body("not ready\n");
        return;
    }
    try response.status(.ok);
    try response.body("ready\n");
}
```

O detalhe de implementação de `std.http.Server` muda entre versões do Zig; o contrato de readiness + drain não muda. Confira a referência de `std.http.Server` e o guia de [proxy reverso e load balancing](/artigos/zig-nginx-proxy-reverso-load-balancing/) para a parte de borda.

## Workers e filas: o que fazer com o job atual

Graceful shutdown em worker é mais delicado que em HTTP porque o “request” pode durar minutos. Defina uma política por tipo de job:

| Tipo de job | No shutdown |
|---|---|
| curto e idempotente (segundos) | terminar |
| longo e idempotente | cancelar e reenfileirar |
| não idempotente (pagamento, e-mail) | preferir terminar se já começou; senão não puxar |
| batch com checkpoint | salvar progresso e sair |

O worker precisa consultar a flag **entre** jobs e, se possível, em pontos de cancelamento internos:

```zig
fn workerLoop(queue: *JobQueue, flag: *const std.atomic.Value(bool)) !void {
    while (!flag.load(.seq_cst)) {
        const job = queue.popOrWait(100 * std.time.ns_per_ms) catch continue;
        try processJob(job, flag);
        queue.ack(job);
    }
    std.log.info("worker_exit drained=true", .{});
}

fn processJob(job: Job, flag: *const std.atomic.Value(bool)) !void {
    // checkpoints: se o shutdown subir no meio, persista estado e devolva o job
    if (flag.load(.seq_cst)) return error.Shutdown;
    try job.step1();
    if (flag.load(.seq_cst)) return error.Shutdown;
    try job.step2();
}
```

Sem idempotência e sem `ack` explícito, “cancelar e reenfileirar” vira duplicata. O artigo de filas e workers e o de [circuit breaker, timeout e retry](/artigos/zig-circuit-breaker-timeout-retry/) fecham a resiliência do lado da dependência externa.

## Timeout: alinhe processo e supervisor

O graceful shutdown só é real se o prazo do programa e o prazo do supervisor combinam.

Exemplos:

```ini
# systemd
TimeoutStopSec=20
KillSignal=SIGTERM
FinalKillSignal=SIGKILL
```

```yaml
# Docker Compose
stop_grace_period: 20s
```

```yaml
# Kubernetes
terminationGracePeriodSeconds: 30
```

Regra prática: **drain interno < TimeoutStopSec**. Se o systemd espera 20s, o seu `drain_ns` deve ser 10–15s, deixando margem para flush de logs e `deinit`. Se o programa esperar 60s e o supervisor matar em 10s, o código de drain nunca roda até o fim.

Também registre o motivo da saída:

- `reason=signal` + drain completo;
- `reason=drain_timeout`;
- `reason=fatal_error` (outro caminho, não confundir com stop).

Isso vira ouro em [logs e Prometheus](/artigos/zig-observabilidade-logs-prometheus/) quando alguém pergunta “por que o job reprocessou no deploy das 3h”.

## O que liberar no `deinit`

Uma checklist típica de limpeza, na ordem:

1. parar de aceitar conexões / jobs;
2. esperar ou cancelar trabalho em voo (com timeout);
3. fechar listener e conexões restantes;
4. flush de buffers de log e métricas;
5. liberar locks de arquivo e PID file;
6. fechar conexões de banco, Redis, sockets Unix;
7. `deinit` de allocators e detectar leak em debug.

Zig deixa leaks e handles visíveis. Use `GeneralPurposeAllocator` em desenvolvimento com detecção de leak no `defer`, e não engula erros de `deinit` sem log. Para arquivos temporários e escrita atômica, o mesmo cuidado do [download com checksum](/artigos/zig-download-arquivos-http-checksum/) vale no stop: não renomeie um parcial como se fosse final.

## Multi-thread e corridas comuns

Com várias threads, a flag atômica resolve o “foi pedido stop?”, mas não resolve sozinha:

- **condition variable / fila bloqueante** — o worker dormindo em `pop` precisa de wake no shutdown (timeout no wait ou sinalização explícita);
- **accept bloqueante** — prefira accept com timeout, event loop ou feche o listener para destravar o `accept`;
- **contador de trabalho ativo** — use atomic ou mutex para `activeWork()` não mentir no drain;
- **double signal** — segundo `SIGTERM` pode acelerar o force cancel; documente se isso é desejado.

Evite `mutex` dentro do signal handler. O handler só toca na atomic; quem pega lock é o caminho normal do programa.

## Testes que realmente provam o shutdown

Teste unitário da flag e do timeout:

```zig
test "drain encerra quando active_work zera" {
    var app = TestApp{ .active = 1 };
    app.requestShutdown();
    app.active = 0;
    try std.testing.expect(app.shouldExit());
}

test "drain_timeout força cancelamento" {
    var app = TestApp{ .active = 3, .now_ms = 0, .deadline_ms = 100 };
    app.requestShutdown();
    app.now_ms = 150;
    try std.testing.expect(app.shouldForceCancel());
}
```

Teste de integração (o que pega bug de verdade):

1. spawn do binário com `std.process.Child` (veja executar processos);
2. enviar carga (HTTP ou jobs);
3. `kill -TERM <pid>`;
4. assert em: log de stop, zero novas aceitações, exit code, ausência de arquivo parcial.

Não dependa só de “subi na mão e deu Ctrl+C”. O caminho de `SIGTERM` do systemd é o que importa em produção.

## Checklist de produção

- [ ] `SIGINT` e `SIGTERM` instalados com o mesmo handler mínimo.
- [ ] Handler só grava flag atômica; zero I/O/alocação no handler.
- [ ] Loop principal loga o início do drain e o motivo da saída.
- [ ] Listener / consumidor para de aceitar trabalho novo imediatamente.
- [ ] Existe contador confiável de trabalho em voo.
- [ ] Timeout de drain < `TimeoutStopSec` / `stop_grace_period`.
- [ ] Readiness falha no shutdown; liveness permanece coerente.
- [ ] Jobs longos têm política de cancel/retry/idempotência.
- [ ] Logs e métricas fazem flush antes do exit.
- [ ] Arquivos temporários não viram artefato final no meio do stop.
- [ ] Teste de integração manda `SIGTERM` de verdade.
- [ ] Unit file / compose / manifest documenta o prazo de stop.

## Erros frequentes

**Chamar `std.process.exit` no handler.** Pula `defer`, pula drain, corrompe estado.

**Logar ou formatar no handler.** Pode dead lock ou corromper allocator sob sinal.

**Esperar “para sempre” o trabalho acabar.** O supervisor manda `SIGKILL` e você perde o log final.

**Tratar só `SIGINT`.** Em produção quem manda é `SIGTERM`.

**Health check único.** Se liveness e readiness são o mesmo endpoint e você devolve 503 no stop, o orquestrador pode matar a réplica cedo demais ou achar que o pod morreu.

**Ignorar workers.** O HTTP drena, mas a thread de fila continua puxando job e o processo “nunca termina”.

## Conclusão

Graceful shutdown em Zig não é um framework: é um contrato curto e explícito. O SO envia `SIGTERM` ou `SIGINT`, o handler marca uma flag atômica, o loop para de aceitar trabalho, o drain roda com timeout e o processo libera recursos antes de sair. Esse fluxo protege deploys, reduz jobs duplicados e torna o stop observável.

Para ir além, encaixe o padrão no serviço systemd, no [servidor HTTP de produção](/artigos/zig-http-server-producao/), nos workers em background e na [observabilidade com logs e Prometheus](/artigos/zig-observabilidade-logs-prometheus/). Quando o stop vira caminho de código — e não surpresa de madrugada — o binário Zig deixa de ser só rápido e passa a ser operável.

## Perguntas frequentes

### O que é graceful shutdown em Zig?

É receber `SIGTERM` ou `SIGINT`, parar trabalho novo, drenar ou cancelar com limite o que já está em andamento, liberar recursos e sair com código previsível, em vez de matar o processo no meio de uma operação.

### Qual a diferença entre SIGTERM e SIGINT?

`SIGINT` costuma vir de `Ctrl+C` no terminal. `SIGTERM` é o sinal padrão de stop do systemd, Docker e Kubernetes. Em produção, trate os dois como pedido de encerramento com o mesmo drain.

### Posso alocar ou fazer I/O no signal handler?

Não. O handler deve ser assíncrono-seguro: grave uma flag atômica e deixe o loop principal reagir com logs, fechamento de sockets e flush de arquivos.

### Quanto tempo esperar no drain?

Use um timeout menor que o do supervisor — em geral 5 a 30 segundos, alinhado a `TimeoutStopSec` ou `stop_grace_period`. Se estourar, registre pendências e encerre.

### Graceful shutdown resolve jobs em background sozinho?

Não. Workers precisam consultar a flag, parar de puxar jobs novos e aplicar política explícita (terminar, cancelar ou reenfileirar) para o job atual.

### Como testar de ponta a ponta?

Suba o processo, gere carga, envie `kill -TERM <pid>` e verifique logs de stop, ausência de novas aceitações, limpeza de recursos e exit code. Automatize com `std.process.Child` em teste de integração.
