---
title: "Zig para Filas e Workers: Jobs em Background com Controle"
url: "https://ziglang.com.br/artigos/zig-filas-workers-background/"
markdown_url: "https://ziglang.com.br/artigos/zig-filas-workers-background.MD"
description: "Como desenhar filas e workers em Zig para tarefas em background: modelo produtor-consumidor, limites, retry, persistência, observabilidade e deploy seguro."
date: "2026-05-21"
author: ""
---

# Zig para Filas e Workers: Jobs em Background com Controle

Como desenhar filas e workers em Zig para tarefas em background: modelo produtor-consumidor, limites, retry, persistência, observabilidade e deploy seguro.


Nem todo trabalho de uma aplicação precisa acontecer dentro da requisição HTTP. Enviar e-mail, gerar relatório, sincronizar API externa, compactar arquivo, processar webhook, atualizar cache, calcular métrica e varrer diretórios são tarefas que costumam ficar melhores em background. Em muitos stacks, a resposta automática é instalar uma fila pesada, adicionar um worker em outra linguagem, configurar Redis, escrever YAML de orquestração e torcer para os retries não duplicarem efeitos colaterais.

Com Zig, dá para pensar de forma mais direta. A linguagem não traz um framework de filas pronto, mas oferece boas peças para montar workers pequenos, previsíveis e fáceis de distribuir: threads, mutexes, condition variables, atomics, allocators explícitos, integração com C, binário único e cross-compilation. O resultado não precisa competir com plataformas completas de mensageria. Ele precisa resolver bem um problema comum: processar tarefas fora do caminho crítico sem esconder custo operacional.

Este artigo mostra como desenhar filas e workers em Zig para jobs em background. O foco é arquitetura prática: quando usar fila em memória, quando persistir em disco, como limitar concorrência, como lidar com retry, como desligar com segurança e como observar o processo em produção. Para aprofundar os blocos técnicos, combine este guia com [padrões avançados de concorrência em Zig](/artigos/zig-concorrencia-padroes-avancados/), [Zig e SQLite para ferramentas locais](/artigos/zig-sqlite-ferramentas-locais/), [servidor HTTP em produção](/artigos/zig-http-server-producao/) e [debugging/profiling com Tracy, Valgrind e perf](/artigos/zig-depuracao-profiling-tracy-valgrind-perf/).

## Onde workers em Zig fazem sentido

Workers em Zig brilham quando o trabalho precisa ser previsível, próximo do sistema e barato de operar. Exemplos bons:

- processador de webhooks que valida assinatura, normaliza payload e grava evento;
- worker de imagens que redimensiona arquivos em lote;
- sincronizador de API que roda a cada minuto e atualiza cache local;
- gerador de índices para busca em arquivos;
- coletor de métricas que lê logs e produz agregados;
- pipeline pequeno de ETL dentro de uma ferramenta interna;
- daemon local que atende uma CLI e processa tarefas demoradas;
- serviço auxiliar dentro de um container mínimo.

O ponto comum é que a tarefa tem fronteiras claras. Entra um job com tipo, payload e prazo. Sai sucesso, falha temporária ou falha definitiva. Zig ajuda porque obriga você a representar essas fronteiras no código, em vez de depender de magia de framework.

Quando a necessidade é fila distribuída com milhares de produtores, consumer groups, replay histórico, garantias transacionais entre serviços e operação multi-região, Kafka, NATS, RabbitMQ ou SQS provavelmente fazem mais sentido. Zig ainda pode ser o worker, mas não precisa reinventar o broker.

## Modelo mínimo: produtor, fila e workers

Um desenho simples tem três partes:

```text
produtor -> fila -> workers
```

O produtor cria jobs. Pode ser uma rota HTTP, uma CLI, um watcher de arquivos ou um agendador. A fila guarda jobs pendentes. Os workers retiram jobs, executam e registram resultado.

Em Zig, uma fila em memória pode começar com `std.ArrayList` ou uma estrutura própria protegida por `std.Thread.Mutex` e acordada por `std.Thread.Condition`. Para carga pequena, isso é suficiente. O segredo é não deixar a fila virar um buraco sem fundo.

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

const Job = struct {
    id: u64,
    kind: []const u8,
    payload: []const u8,
    attempts: u8 = 0,
};

const JobQueue = struct {
    mutex: std.Thread.Mutex = .{},
    cond: std.Thread.Condition = .{},
    jobs: std.ArrayList(Job),
    closing: bool = false,

    fn init(allocator: std.mem.Allocator) JobQueue {
        return .{ .jobs = std.ArrayList(Job).init(allocator) };
    }

    fn push(self: *JobQueue, job: Job) !void {
        self.mutex.lock();
        defer self.mutex.unlock();

        try self.jobs.append(job);
        self.cond.signal();
    }

    fn pop(self: *JobQueue) ?Job {
        self.mutex.lock();
        defer self.mutex.unlock();

        while (self.jobs.items.len == 0 and !self.closing) {
            self.cond.wait(&self.mutex);
        }

        if (self.jobs.items.len == 0) return null;
        return self.jobs.orderedRemove(0);
    }
};
```

Esse exemplo é didático, não a versão final para alta vazão. `orderedRemove(0)` desloca elementos e fica caro em filas grandes. Para produção, use fila circular, linked list ou estrutura dedicada. Mas o modelo deixa claro o contrato: `push` adiciona, `pop` espera, `closing` permite desligamento limpo.

## Limites antes de performance

O erro mais comum em sistemas de background não é falta de performance. É falta de limite. Uma fila sem limite transforma pico de entrada em consumo infinito de memória. Um worker sem timeout fica preso em API externa. Retry sem teto vira loop eterno. Payload sem tamanho máximo vira ataque de alocação.

Antes de otimizar, defina limites explícitos:

- tamanho máximo da fila em memória;
- tamanho máximo do payload;
- número máximo de workers;
- timeout por job;
- número máximo de tentativas;
- intervalo de retry com backoff;
- política para descartar, arquivar ou mover para dead letter;
- orçamento de memória por job.

Zig combina bem com essa disciplina porque alocação é visível. Se cada job recebe uma arena própria, toda memória temporária pode ser liberada no fim da execução. Essa estratégia já aparece em servidores HTTP e servidores MCP: uma arena por requisição ou chamada, descartada de uma vez depois do trabalho.

```zig
fn runJob(parent_allocator: std.mem.Allocator, job: Job) !void {
    var arena = std.heap.ArenaAllocator.init(parent_allocator);
    defer arena.deinit();

    const allocator = arena.allocator();
    _ = allocator;

    if (std.mem.eql(u8, job.kind, "sync_api")) {
        try syncApi(job.payload);
    } else if (std.mem.eql(u8, job.kind, "rebuild_index")) {
        try rebuildIndex(job.payload);
    } else {
        return error.UnknownJobKind;
    }
}
```

O benefício operacional é simples: mesmo que o job crie buffers temporários, strings parseadas e estruturas auxiliares, a arena limpa tudo ao final. Isso não substitui cuidado com recursos externos, como arquivos e sockets, mas reduz vazamentos acidentais.

## Fila em memória ou persistente?

Fila em memória é boa para tarefas descartáveis ou recalculáveis. Se o processo cair, os jobs somem. Isso pode ser aceitável para atualizar cache, recalcular índice local ou processar algo que será reenfileirado por outro mecanismo.

Quando o job representa trabalho que não pode desaparecer, persista. SQLite é uma opção prática para um worker pequeno: um arquivo local, transações, índices e sem servidor separado. O artigo sobre [Zig e SQLite](/artigos/zig-sqlite-ferramentas-locais/) cobre o encaixe da linguagem com banco embutido; para filas, a modelagem pode ser simples:

```sql
CREATE TABLE jobs (
  id INTEGER PRIMARY KEY,
  kind TEXT NOT NULL,
  payload TEXT NOT NULL,
  status TEXT NOT NULL,
  attempts INTEGER NOT NULL DEFAULT 0,
  run_after TEXT NOT NULL,
  last_error TEXT,
  created_at TEXT NOT NULL,
  updated_at TEXT NOT NULL
);
```

O worker busca jobs `pending` com `run_after <= now`, marca como `running`, executa e depois grava `done` ou `failed`. Para evitar que uma queda deixe job preso em `running`, registre heartbeat ou `locked_at` e recupere jobs antigos na inicialização.

Essa abordagem não é tão poderosa quanto um broker distribuído, mas é excelente para CLIs, agentes locais, ferramentas internas e serviços pequenos. Também simplifica debug: o estado está em um arquivo consultável.

## Retry precisa de idempotência

Retry é necessário, mas perigoso. Se um job envia cobrança, e-mail, webhook ou atualização externa, repetir pode causar efeito duplicado. A solução não é abandonar retry; é desenhar o job para ser idempotente.

Um job idempotente pode rodar duas vezes e produzir o mesmo estado final. Isso pode exigir:

- chave externa única por operação;
- tabela de deduplicação;
- verificação antes de escrever;
- operação `upsert` em vez de `insert` cego;
- arquivo temporário seguido de rename atômico;
- marcação clara de `started`, `done` e `failed`.

Em Zig, esse cuidado aparece no design das APIs internas. Em vez de `enviar_email(payload)`, prefira uma função que recebe `job_id`, `recipient`, `template` e uma chave de deduplicação. Em vez de salvar resultado direto no caminho final, escreva em arquivo temporário e mova no fim. O worker não deve depender de sorte.

Use backoff simples: primeira falha tenta depois de alguns segundos; próximas falhas aumentam o intervalo; depois de um teto, mova para falha definitiva. Guarde `last_error`, mas evite salvar segredos em logs ou payloads.

## Workers dentro ou fora do servidor HTTP?

Para projetos pequenos, rodar produtor e worker no mesmo processo pode ser suficiente. Uma rota HTTP recebe pedido, valida payload e chama `queue.push`. Threads de worker vivem no mesmo binário. O deploy fica simples: um processo, uma imagem, uma configuração.

Essa simplicidade tem custo. Se a aplicação HTTP precisa escalar separadamente dos workers, se jobs consomem muita CPU, se há risco de travar o processo inteiro ou se você precisa pausar workers sem derrubar API, separe. O mesmo código Zig pode gerar dois binários: `api` e `worker`, compartilhando módulo de domínio, parser e definição de job.

Essa separação conversa bem com [Docker para Zig](/artigos/zig-docker-containers/) e [GitHub Actions para release multiplataforma](/artigos/zig-github-actions-release-multiplataforma/). O pipeline compila os dois binários, empacota artefatos e publica versões previsíveis. Em produção, o container do worker pode ter limites de CPU/memória próprios e uma política de restart diferente da API.

## Observabilidade sem framework pesado

Worker invisível é problema. O mínimo aceitável em produção:

- log estruturado com `job_id`, `kind`, `attempt`, duração e resultado;
- contador de jobs pendentes, em execução, concluídos e falhos;
- medição de tempo por tipo de job;
- alerta para fila crescendo por muito tempo;
- endpoint ou comando de health check;
- forma de inspecionar dead letters.

Você não precisa começar com um stack completo. `std.log` bem usado, métricas exportadas em texto e um comando administrativo já resolvem muito. Se o worker roda junto de um servidor HTTP, uma rota `/readyz` pode verificar se a fila aceita jobs e se dependências obrigatórias estão disponíveis. O artigo sobre [Zig HTTP Server em produção](/artigos/zig-http-server-producao/) entra nos detalhes de health checks, limites e shutdown.

Para performance, meça antes. Um worker com fila em memória pode parecer lento por causa de I/O externo, DNS, banco, compressão ou lock mal posicionado. Use [benchmarking em Zig](/artigos/zig-benchmarking-medir-performance/) e profiling antes de trocar arquitetura.

## Shutdown limpo

Deploys e restarts acontecem. Um worker correto precisa parar de aceitar novos jobs, terminar ou devolver o job atual para a fila, gravar estado e encerrar. Em fila persistente, isso significa marcar jobs interrompidos para retry. Em fila em memória, significa aceitar que jobs pendentes podem ser perdidos ou implementar drenagem antes de sair.

O fluxo recomendado:

1. Receber sinal de encerramento.
2. Marcar `closing = true`.
3. Acordar workers bloqueados na condition variable.
4. Parar produtores ou rejeitar novos jobs.
5. Esperar workers ativos até um timeout.
6. Persistir ou reenfileirar o que não terminou.
7. Encerrar com código coerente.

Esse contrato vale mais do que micro-otimização. Um worker que encerra corretamente é mais fácil de operar em systemd, Docker, Kubernetes ou qualquer scheduler simples.

## Comparando com Go e Rust

Go é muito forte para workers porque goroutines, channels e biblioteca padrão tornam concorrência simples. Em muitos times, Go será a escolha mais rápida para fila interna. Para comparar a cultura operacional, o <a href="https://golang.com.br/" target="_blank" rel="noopener noreferrer" onclick="umami.track('portfolio-site-click', { destination: 'golang.com.br' })">Golang Brasil</a> tem materiais sobre backend, concorrência e serviços em produção.

Rust oferece garantias de memória e concorrência mais rígidas em tempo de compilação, além de ecossistema async maduro. O custo é maior complexidade de tipos, lifetimes e runtime async quando o domínio já é operacionalmente complexo.

Zig fica em outro ponto do mapa: menos framework, menos runtime, mais controle explícito. Para workers pequenos, binários distribuíveis e integração com C ou sistema operacional, isso é uma vantagem real. Para produto que precisa de ecossistema pronto, SDKs numerosos e onboarding mais comum, Go ou Rust podem reduzir atrito.

## Checklist de produção

Antes de colocar um worker Zig em produção, revise:

- a fila tem limite claro de tamanho;
- payload tem tamanho máximo;
- cada tipo de job tem timeout;
- retry tem backoff e teto;
- operações externas são idempotentes;
- falhas definitivas ficam inspecionáveis;
- logs não vazam tokens ou dados sensíveis;
- shutdown foi testado;
- build usa `ReleaseSafe` por padrão;
- métricas mostram backlog e duração;
- deploy separa API e worker quando necessário.

Esse checklist evita a maior parte dos incidentes comuns. A diferença entre uma fila útil e uma fonte de dor raramente está na linguagem. Está nos contratos operacionais.

## Conclusão

Zig não tenta vender uma abstração mágica para background jobs. Isso é bom. Filas e workers são infraestrutura, e infraestrutura precisa de limites claros, estado observável e comportamento previsível em falha.

Use fila em memória quando o trabalho é pequeno e descartável. Use persistência quando perder job é inaceitável. Separe workers do servidor HTTP quando escala, risco ou operação pedirem. Modele retry com idempotência desde o começo. E trate desligamento limpo como parte do design, não como detalhe final.

Para times que querem binários pequenos, baixo runtime e controle sobre alocação, Zig é uma opção muito forte para workers de infraestrutura. Não porque elimina complexidade, mas porque deixa a complexidade visível o suficiente para ser operada.
