---
title: "Arquitetura Event-Driven em Zig: Event Loop, Filas e Backpressure"
url: "https://ziglang.com.br/artigos/zig-event-driven/"
markdown_url: "https://ziglang.com.br/artigos/zig-event-driven.MD"
description: "Como desenhar sistemas event-driven em Zig: event loop, pub/sub, filas, backpressure, retries, io_uring, observabilidade e deploy em produção."
date: "2026-02-21"
author: "Zig Brasil"
---

# Arquitetura Event-Driven em Zig: Event Loop, Filas e Backpressure

Como desenhar sistemas event-driven em Zig: event loop, pub/sub, filas, backpressure, retries, io_uring, observabilidade e deploy em produção.


# Arquitetura Event-Driven em Zig: Event Loop, Filas e Backpressure

Arquitetura orientada a eventos aparece em quase todo sistema que precisa reagir rápido sem desperdiçar recursos: servidores HTTP, gateways de rede, processadores de filas, pipelines de dados, telemetria, automação industrial e ferramentas locais que observam arquivos ou sockets. Em vez de uma sequência linear de chamadas bloqueantes, o programa recebe eventos, decide o que fazer e agenda trabalho para handlers pequenos.

Zig combina bem com esse estilo porque deixa alocação, vida útil de buffers, concorrência e I/O explícitos. A linguagem não força um runtime assíncrono específico. Você pode começar com uma fila em memória, evoluir para threads e `poll`, integrar `io_uring` no Linux, ou delegar parte da mensageria para NATS, Redis Streams, Kafka ou outro broker. O ponto é desenhar a fronteira entre evento, fila e handler antes de escrever código demais.

Este guia atualiza a visão prática de event-driven em Zig: quando usar, como montar um event loop simples, onde aplicar backpressure, como tratar erros, como observar o sistema e quais limites importam em produção.

## Quando event-driven faz sentido em Zig

Use uma arquitetura event-driven quando o sistema recebe muitos estímulos independentes e precisa manter latência previsível. Exemplos comuns:

- servidor HTTP que transforma cada requisição em trabalho interno;
- daemon que observa conexões TCP, arquivos, timers e sinais do sistema;
- pipeline que consome mensagens de uma fila e grava em banco;
- gateway que multiplexa milhares de conexões com pouco consumo de memória;
- agente de observabilidade que coleta métricas, logs e eventos de rede;
- sistema embarcado que reage a interrupções, timers e sensores.

Evite event-driven quando o fluxo é pequeno, batch, linear e fácil de testar como uma sequência simples. Zig não recompensa arquitetura acidentalmente complexa. Se um `main` com três funções resolve, mantenha simples. Event-driven vale quando fila, isolamento de handlers e controle de concorrência reduzem acoplamento ou protegem o sistema contra picos.

## Modelo mental: evento, fila, handler e efeito

Um desenho saudável separa quatro responsabilidades:

1. **Evento**: fato pequeno e imutável, como `requisicao_http`, `timer_expirado`, `mensagem_recebida` ou `socket_pronto`.
2. **Fila**: buffer que guarda eventos até serem processados, com política clara para quando enche.
3. **Handler**: função curta que interpreta um evento e produz efeito ou novos eventos.
4. **Efeito**: I/O, gravação em banco, chamada externa, resposta HTTP, log ou métrica.

Essa separação evita o erro mais comum: deixar o callback fazer tudo, bloquear por tempo indeterminado e alocar memória sem limite. Em Zig, a assinatura do handler deve deixar dependências explícitas: allocator, logger, cliente HTTP, pool de banco, config, arena temporária e contexto de shutdown.

```text
socket/timer/broker
      │
      ▼
  evento pequeno
      │
      ▼
 fila com limite ── backpressure/drop/retry
      │
      ▼
 handler curto
      │
      ├── efeito externo
      └── novos eventos
```

## Event loop mínimo em Zig

Um event loop didático pode ser apenas uma fila em memória. Ele não substitui `poll`, `epoll`, `kqueue` ou `io_uring`, mas ajuda a organizar contratos.

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

const TipoEvento = enum {
    requisicao_http,
    mensagem_recebida,
    timer_expirado,
    conexao_fechada,
    shutdown,
};

const Evento = struct {
    tipo: TipoEvento,
    payload: []const u8,
    created_at_ms: i64,
};

const Handler = *const fn (ctx: *Contexto, evento: Evento) anyerror!void;

const Contexto = struct {
    allocator: std.mem.Allocator,
    logger: std.log.Level = .info,
    shutting_down: bool = false,
};

const EventLoop = struct {
    allocator: std.mem.Allocator,
    fila: std.ArrayList(Evento),
    handlers: std.AutoHashMap(TipoEvento, Handler),
    max_queue: usize,

    pub fn init(allocator: std.mem.Allocator, max_queue: usize) EventLoop {
        return .{
            .allocator = allocator,
            .fila = std.ArrayList(Evento).init(allocator),
            .handlers = std.AutoHashMap(TipoEvento, Handler).init(allocator),
            .max_queue = max_queue,
        };
    }

    pub fn deinit(self: *EventLoop) void {
        self.handlers.deinit();
        self.fila.deinit();
    }

    pub fn on(self: *EventLoop, tipo: TipoEvento, handler: Handler) !void {
        try self.handlers.put(tipo, handler);
    }

    pub fn emitir(self: *EventLoop, evento: Evento) !void {
        if (self.fila.items.len >= self.max_queue) return error.QueueFull;
        try self.fila.append(evento);
    }

    pub fn tick(self: *EventLoop, ctx: *Contexto) !void {
        if (self.fila.items.len == 0) return;

        const evento = self.fila.orderedRemove(0);
        if (evento.tipo == .shutdown) {
            ctx.shutting_down = true;
            return;
        }

        if (self.handlers.get(evento.tipo)) |handler| {
            handler(ctx, evento) catch |err| {
                std.log.err("handler failed: tipo={s} err={s}", .{
                    @tagName(evento.tipo),
                    @errorName(err),
                });
            };
        }
    }
};
```

O detalhe importante é o limite da fila. Sem `max_queue`, o sistema pode transformar pico de tráfego em consumo ilimitado de memória. Em produção, fila sem limite é dívida técnica disfarçada.

## Backpressure: o que acontece quando a fila enche

Todo sistema event-driven precisa responder a uma pergunta desconfortável: o que fazer quando chegam mais eventos do que os handlers conseguem processar? Há quatro respostas típicas:

- **rejeitar**: retornar 429/503, fechar conexão ou recusar mensagem;
- **degradar**: processar só eventos essenciais e descartar telemetria secundária;
- **bufferizar fora do processo**: usar broker ou disco para absorver picos;
- **aumentar workers**: paralelizar handlers quando o gargalo permite.

A pior resposta é fingir que não existe limite. Em Zig, prefira tornar o limite visível no tipo ou na configuração: `max_queue`, `max_body_bytes`, `max_inflight`, `max_retries`, `timeout_ms`. O mesmo raciocínio vale para [servidores HTTP em produção](/artigos/zig-http-server-producao/): limite de corpo, timeout e health check são partes da arquitetura, não detalhes de deploy.

## Pub/sub interno sem virar framework

Para muitos serviços pequenos, um pub/sub interno basta. Eventos de domínio podem acionar logs, métricas, atualização de cache ou persistência sem acoplar tudo ao handler principal.

```zig
const Assinante = struct {
    tipo: TipoEvento,
    handler: Handler,
};

fn publicar(ctx: *Contexto, assinantes: []const Assinante, evento: Evento) void {
    for (assinantes) |sub| {
        if (sub.tipo != evento.tipo) continue;
        sub.handler(ctx, evento) catch |err| {
            std.log.warn("subscriber failed: tipo={s} err={s}", .{
                @tagName(evento.tipo),
                @errorName(err),
            });
        };
    }
}
```

Mantenha esse mecanismo pequeno. Quando precisa de durabilidade, fan-out entre processos, reprocessamento, ordenação por chave ou auditoria, use uma fila externa. O Zig continua útil como consumidor eficiente e previsível, mas o broker passa a carregar semânticas que não deveriam ficar em memória local.

## Filas lock-free: use com critério

Zig permite escrever estruturas lock-free com atomics, mas isso não deve ser o primeiro passo. Uma fila protegida por mutex pode ser suficiente e muito mais fácil de auditar. Lock-free faz sentido quando você mediu contenção e sabe que o custo de complexidade compensa.

Um buffer circular single-producer/single-consumer ilustra a ideia:

```zig
fn RingSpsc(comptime T: type, comptime capacidade: usize) type {
    return struct {
        buffer: [capacidade]T = undefined,
        write_pos: std.atomic.Value(usize) = std.atomic.Value(usize).init(0),
        read_pos: std.atomic.Value(usize) = std.atomic.Value(usize).init(0),

        const Self = @This();

        pub fn push(self: *Self, value: T) bool {
            const wp = self.write_pos.load(.monotonic);
            const next = (wp + 1) % capacidade;
            if (next == self.read_pos.load(.acquire)) return false;
            self.buffer[wp] = value;
            self.write_pos.store(next, .release);
            return true;
        }

        pub fn pop(self: *Self) ?T {
            const rp = self.read_pos.load(.monotonic);
            if (rp == self.write_pos.load(.acquire)) return null;
            const value = self.buffer[rp];
            self.read_pos.store((rp + 1) % capacidade, .release);
            return value;
        }
    };
}
```

Documente o contrato: esse exemplo é SPSC, não MPMC. Se dois produtores chamarem `push` simultaneamente, a estrutura deixa de ser segura. Para MPMC, use desenho específico, testes agressivos e benchmarks reais. A página de [code review em Zig](/artigos/zig-code-review-checklist/) é uma boa checklist para revisar atomics, ownership e cleanup.

## I/O: poll, epoll, kqueue e io_uring

O loop acima organiza eventos internos. Para I/O real, você precisa observar descritores, timers ou callbacks do sistema operacional.

- Em projetos portáveis, `poll`/`select` podem bastar para começar.
- No Linux, `epoll` é um caminho conhecido para muitos sockets.
- Em BSD/macOS, `kqueue` é o equivalente comum.
- No Linux moderno, `io_uring` permite submeter operações assíncronas com menos overhead em alguns cenários.

Zig não obriga você a escolher uma abstração única. Isso é poder e responsabilidade. Se o produto precisa rodar em Linux apenas, `io_uring` pode ser atraente. Se precisa compilar para vários ambientes, mantenha uma camada fina que esconda o backend de I/O e teste cada plataforma separadamente.

Para networking básico antes de entrar em event loops sofisticados, comece pelo guia de [sockets TCP e UDP em Zig](/artigos/zig-networking-sockets-tcp-udp/).

## Erros, retries e dead letters

Eventos falham. Broker cai, banco retorna timeout, JSON vem inválido, downstream responde 500, handler encontra bug. A arquitetura precisa diferenciar erro transitório de erro permanente.

Um padrão prático:

- erro de validação: rejeite e registre contexto mínimo;
- timeout externo: retry com backoff e jitter;
- falha repetida: mande para dead letter ou estado de inspeção;
- bug interno: logue com correlação, alerte e evite loop infinito;
- shutdown: pare de aceitar novos eventos e drene o que for seguro.

Em Zig, use error sets para expressar essas categorias. Não transforme todo erro em `anyerror` até a borda sem registrar intenção. O guia de [error handling em Zig](/artigos/zig-error-handling-boas-praticas/) cobre `try`, `catch`, `errdefer` e cleanup em mais detalhe.

## Observabilidade para sistemas event-driven

Sem observabilidade, event-driven vira caixa-preta. Métricas mínimas:

- tamanho atual da fila;
- eventos recebidos por tipo;
- eventos processados por tipo;
- erros por handler;
- latência entre criação e processamento;
- número de retries;
- eventos descartados por backpressure;
- tempo de shutdown/drain.

Logs devem carregar um identificador de correlação quando o evento nasceu de requisição externa. Traces ajudam quando um evento gera outros eventos ou chama serviços diferentes. Para um desenho completo de logs, métricas, traces e health checks, veja [observabilidade em Zig](/artigos/zig-observabilidade/).

## Deploy: processo único, workers ou broker?

A escolha de deploy depende do tipo de garantia que você precisa.

**Processo único com fila em memória** é simples, rápido e bom para ferramentas locais, agentes pequenos e serviços em que perder eventos em restart é aceitável.

**Processo com workers internos** aumenta throughput, mas exige cuidado com ordem, concorrência, compartilhamento de allocator, shutdown e idempotência.

**Broker externo** adiciona operação, mas oferece durabilidade, replay, fan-out e escala horizontal. É o caminho natural quando eventos representam pedidos de cliente, cobrança, integração importante ou processamento caro.

Para serviços cloud native, combine esse desenho com readiness/liveness, limites de memória, logs estruturados e deploy reversível. O guia de [Zig em ambientes cloud native](/artigos/zig-cloud-native/) e o de [Kubernetes Operators em Zig](/artigos/kubernetes-operators-em-zig-crds-watch-reconcile-e-deploy/) mostram os trade-offs de operar binários Zig em cluster.

## Checklist de produção

Antes de colocar um sistema event-driven em produção, confirme:

- cada fila tem limite explícito;
- existe política para fila cheia;
- handlers têm timeout ou orçamento de trabalho;
- eventos grandes usam referência ou armazenamento externo, não cópia ilimitada;
- retry tem limite, backoff e jitter;
- erro permanente não entra em loop infinito;
- shutdown para entrada nova e drena trabalho seguro;
- métricas expõem fila, latência, erro e drop;
- logs têm correlação por requisição/evento;
- testes cobrem ordem, duplicidade, falha e reprocessamento;
- benchmarks medem throughput e p95/p99, não só média.

## Zig vs Go e Rust para event-driven

Para comparar modelos de concorrência event-driven, <a href="https://golang.com.br/" target="_blank" rel="noopener" onclick="umami.track('portfolio-site-click', { destination: 'golang.com.br' })">Go</a> oferece goroutines e channels como primitivos de alto nível. É excelente para serviços de rede comuns e times que querem produtividade imediata.

<a href="https://rustlang.com.br/" target="_blank" rel="noopener" onclick="umami.track('portfolio-site-click', { destination: 'rustlang.com.br' })">Rust</a> conta com Tokio e um ecossistema assíncrono maduro, com garantias fortes de ownership e concorrência. O custo é aprender o modelo async, lifetimes e bibliotecas.

Zig fica no meio: menos framework, mais controle direto. É uma boa escolha quando o sistema valoriza binário pequeno, previsibilidade, integração C, controle de memória e um desenho que a equipe consegue auditar sem runtime mágico.

## FAQ

### Zig tem async/await estável para esse tipo de arquitetura?

O histórico de async em Zig mudou ao longo das versões, então trate exemplos antigos com cuidado. Em produção, prefira desenhar filas, workers, threads e backends de I/O explícitos conforme a versão do compilador e o alvo. Não dependa de tutorial antigo sem validar no Zig atual.

### Event-driven em Zig precisa de io_uring?

Não. `io_uring` é uma opção poderosa no Linux, mas muitos sistemas funcionam bem com threads, `poll`, `epoll`, broker externo ou fila em memória. Use `io_uring` quando a necessidade de I/O assíncrono e o ambiente Linux justificarem a complexidade.

### Como testar um sistema event-driven?

Separe handlers puros de efeitos externos. Teste handlers com eventos sintéticos, injete relógio/cliente/banco falsos, cubra duplicidade e ordem fora do esperado, e rode testes de carga para fila cheia. Para bugs difíceis, combine logs estruturados com métricas de fila e traces.

### Posso usar Kafka, NATS ou Redis Streams com Zig?

Sim, mas a maturidade das bibliotecas varia. Em muitos casos, vale integrar por protocolo simples, cliente C, processo sidecar ou uma ponte em outra linguagem. O importante é manter idempotência, backpressure e observabilidade no consumidor Zig.

## Conteúdo relacionado

- [Zig HTTP Server em Produção](/artigos/zig-http-server-producao/) — limites, logs, health check e deploy.
- [Observabilidade em Zig](/artigos/zig-observabilidade/) — métricas, logs, traces e alertas.
- [Networking com Sockets TCP e UDP](/artigos/zig-networking-sockets-tcp-udp/) — base de I/O de rede.
- [Error Handling em Zig](/artigos/zig-error-handling-boas-praticas/) — erros, cleanup e retries.
- [Sistemas de Tempo Real com Zig](/artigos/zig-real-time-systems/) — latência previsível.
- [Concorrência em Zig](/tutoriais/concorrencia-em-zig/) — threads e atomics.
- [io_uring com Zig](/tutoriais/zig-async-iouring/) — I/O assíncrono no Linux.
- [Zig em Telecomunicações](/cases/case-zig-telecom/) — event-driven em telecom.
