---
title: "Cron Jobs em Zig: Timers, Locks e Rotinas de Produção"
url: "https://ziglang.com.br/artigos/zig-cron-jobs-producao/"
markdown_url: "https://ziglang.com.br/artigos/zig-cron-jobs-producao.MD"
description: "Como criar um agendador de tarefas em Zig para cron jobs locais, timers, retries, logs e execução segura de rotinas operacionais sem framework pesado."
date: "2026-06-15"
author: ""
---

# Cron Jobs em Zig: Timers, Locks e Rotinas de Produção

Como criar um agendador de tarefas em Zig para cron jobs locais, timers, retries, logs e execução segura de rotinas operacionais sem framework pesado.


Todo time que coloca software em produção acaba criando cron jobs e rotinas recorrentes. Limpar arquivos temporários, reconciliar dados, chamar uma API externa, gerar relatórios, compactar logs, atualizar um cache local, enviar métricas, rodar uma verificação de saúde ou processar uma fila pequena. Em muitos projetos isso começa como um shell script no `cron`. Depois aparecem retries, logs, lock para evitar duas execuções simultâneas, configuração por ambiente e um jeito confiável de desligar o processo.

Zig é uma boa opção para esse tipo de ferramenta porque gera binário único, inicia rápido, consome pouca memória e deixa explícito onde tempo, alocação e erro entram no desenho. Você não precisa transformar um job local em uma plataforma distribuída. Muitas vezes o melhor caminho é um executável pequeno, testável e operado por `cron`, systemd timer, Kubernetes CronJob ou uma CLI interna.

Este guia mostra como desenhar **cron jobs em Zig** para rotinas locais e operacionais: quando delegar o relógio ao sistema, quando manter um loop com timers, como evitar sobreposição, como registrar resultado, como lidar com retry e como conectar isso aos guias de [ferramentas internas em Zig para DevOps](/artigos/zig-ferramentas-internas-devops/), [filas e workers em background](/artigos/zig-filas-workers-background/), [configuração segura com segredos](/artigos/zig-configuracao-segura-segredos-env/) e [observabilidade em Zig](/artigos/zig-observabilidade/).

## Primeiro: cron externo ou scheduler dentro do processo?

A decisão mais importante não é a sintaxe de tempo. É quem manda no ciclo de vida.

| Opção | Melhor uso | Vantagem | Cuidado |
|---|---|---|---|
| `cron` do sistema | uma tarefa isolada por horário | simples, conhecido, fácil de auditar | logs e ambiente podem ser pobres |
| systemd timer | servidor Linux próprio | controle de usuário, logs no journal, lock por unidade | exige conhecer systemd |
| Kubernetes CronJob | tarefa em cluster | isolamento, histórico, recursos, secrets | cuidado com concorrência e deadline |
| loop interno em Zig | daemon local, polling, várias rotinas | lógica centralizada e testável | precisa tratar shutdown, drift e falhas |

Para uma rotina diária que roda e termina, prefira agendamento externo. Deixe o Zig cuidar do trabalho: validar config, executar, logar e sair com código correto. Para um agente local que precisa acordar a cada poucos segundos, observar diretórios ou coordenar várias tarefas curtas, um loop interno pode fazer sentido.

A regra prática: **se o processo não precisa ficar vivo, não invente daemon**. Um binário chamado pelo `cron` é mais fácil de operar que um serviço permanente com bug de memória, timer e sinal.

## Esqueleto de job idempotente

Um job recorrente deve ser seguro para rodar de novo. Isso não significa que ele nunca altera estado. Significa que uma repetição depois de falha não duplica efeitos perigosos.

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

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer {
        const status = gpa.deinit();
        if (status == .leak) std.log.err("vazamento de memória detectado", .{});
    }

    const allocator = gpa.allocator();
    const started_ns = std.time.nanoTimestamp();

    const result = runJob(allocator) catch |err| {
        std.log.err("job falhou: {}", .{err});
        std.process.exit(1);
    };

    const elapsed_ms = @divTrunc(std.time.nanoTimestamp() - started_ns, std.time.ns_per_ms);
    std.log.info("job concluído: itens={d} duração_ms={d}", .{ result.items, elapsed_ms });
}

const JobResult = struct {
    items: usize,
};

fn runJob(allocator: std.mem.Allocator) !JobResult {
    _ = allocator;
    // 1. carregar configuração
    // 2. buscar trabalho pendente
    // 3. processar com limites
    // 4. persistir marcador de progresso
    return .{ .items = 0 };
}
```

Mesmo esse esqueleto simples já tem decisões importantes: alocador explícito, erro vira exit code, tempo de execução aparece no log e a função principal do job pode ser testada separadamente.

## Evite duas execuções simultâneas

O problema clássico de cron é sobreposição. Uma tarefa agendada a cada cinco minutos demora oito minutos. A próxima começa antes da anterior terminar. Dependendo da rotina, isso duplica chamadas externas, prende arquivos, corrompe relatório ou disputa banco.

A defesa mínima é um lock. Em Linux, você pode delegar isso ao `flock` no próprio cron:

```cron
*/5 * * * * flock -n /tmp/zig-sync.lock /opt/jobs/zig-sync
```

Quando o lock precisa ser portátil ou fazer parte do binário, use arquivo de lock com criação exclusiva. O detalhe é não tratar lock como segurança perfeita: se o processo morre, o arquivo pode sobrar. Por isso o lock deve registrar PID, horário e, se fizer sentido, expirar com cuidado.

```zig
fn acquireLock(path: []const u8) !std.fs.File {
    return std.fs.cwd().createFile(path, .{
        .read = true,
        .exclusive = true,
    }) catch |err| switch (err) {
        error.PathAlreadyExists => return error.JobAlreadyRunning,
        else => return err,
    };
}
```

Em systemd, prefira modelar a unidade para não sobrepor. Em Kubernetes CronJob, configure `concurrencyPolicy: Forbid` quando a rotina não pode rodar em paralelo. O ponto é o mesmo: a proteção deve existir fora da lógica de negócio.

## Timers internos sem drift invisível

Se você precisa de um daemon simples, evite somar atrasos ao intervalo. Um loop ingênuo faz trabalho, dorme 60 segundos, faz trabalho, dorme 60 segundos. Se o trabalho demora 20 segundos, o ciclo real vira 80 segundos. Às vezes isso é aceitável. Às vezes cria drift acumulado.

Para polling comum, a forma simples é suficiente:

```zig
fn runLoop(allocator: std.mem.Allocator) !void {
    while (true) {
        const started = std.time.milliTimestamp();
        try runOnce(allocator);

        const elapsed = std.time.milliTimestamp() - started;
        std.log.info("ciclo concluído em {d}ms", .{elapsed});

        std.time.sleep(60 * std.time.ns_per_s);
    }
}
```

Para horários fixos, calcule o próximo instante-alvo e durma até ele. Isso evita que um atraso pequeno mova todos os ciclos seguintes.

```zig
fn sleepUntil(next_ms: i64) void {
    const now = std.time.milliTimestamp();
    if (next_ms <= now) return;

    const delta_ms: u64 = @intCast(next_ms - now);
    std.time.sleep(delta_ms * std.time.ns_per_ms);
}
```

Em produção, registre quando o job começou atrasado. Atraso recorrente é sinal de que o intervalo está agressivo demais, a tarefa ficou lenta ou o host está saturado.

## Retry com limite e backoff

Jobs recorrentes falham por motivos normais: DNS, API externa, arquivo temporariamente indisponível, banco reiniciando, rate limit. O erro ruim não é falhar. É repetir agressivamente até piorar o incidente.

Use retry curto dentro da execução e deixe o próximo agendamento tentar de novo depois. Um padrão simples:

```zig
fn withRetry(comptime F: type, func: F) !void {
    var attempt: u8 = 0;
    while (attempt < 3) : (attempt += 1) {
        func() catch |err| {
            if (attempt == 2) return err;
            const delay_ms: u64 = 250 * (@as(u64, attempt) + 1);
            std.log.warn("tentativa {d} falhou: {}; aguardando {d}ms", .{ attempt + 1, err, delay_ms });
            std.time.sleep(delay_ms * std.time.ns_per_ms);
            continue;
        };
        return;
    }
}
```

Para tarefas que chamam HTTP, combine isso com limites de tempo. Um retry sem timeout não é retry; é uma fila de threads presas. O guia de [circuit breaker, timeout e retry em Zig](/artigos/zig-circuit-breaker-timeout-retry/) aprofunda essa parte para clientes HTTP e workers.

## Estado: marcador pequeno, não banco improvisado

Um job diário muitas vezes precisa lembrar o último item processado. Não grave estado como texto solto se a tarefa tem consequência de negócio. Use um formato simples e validável: JSON pequeno, SQLite local ou a própria tabela do sistema que você está reconciliando.

Para estado local mínimo:

```zig
const State = struct {
    last_id: u64,
    last_run_unix: i64,
};

fn loadState(allocator: std.mem.Allocator, path: []const u8) !State {
    const data = std.fs.cwd().readFileAlloc(allocator, path, 64 * 1024) catch |err| switch (err) {
        error.FileNotFound => return .{ .last_id = 0, .last_run_unix = 0 },
        else => return err,
    };
    defer allocator.free(data);

    return try std.json.parseFromSliceLeaky(State, allocator, data, .{});
}
```

Para estado com múltiplas linhas, retries por item e auditoria, SQLite é mais honesto. O artigo sobre [Zig e SQLite para ferramentas locais](/artigos/zig-sqlite-ferramentas-locais/) mostra por que um banco embutido combina bem com CLIs operacionais: transações, índices e inspeção manual quando algo dá errado.

## Logs que ajudam no dia seguinte

Logs de cron costumam ser esquecidos até o dia em que você precisa deles. Faça o binário imprimir eventos estruturados o suficiente para responder quatro perguntas:

1. quando começou e terminou;
2. quantos itens tentou, processou, pulou e falhou;
3. qual configuração relevante estava ativa, sem vazar segredo;
4. qual erro causou exit code diferente de zero.

Evite logar tokens, URLs assinadas, payloads sensíveis e dados pessoais. Para segredos, registre apenas o nome lógico da configuração, como `API_TOKEN presente`, não o valor. Isso segue a mesma disciplina do guia de [configuração segura com segredos e env vars](/artigos/zig-configuracao-segura-segredos-env/).

Se a rotina roda em systemd, `std.log` indo para stdout/stderr já cai no journal. Em Kubernetes CronJob, stdout vira log do pod. Em cron clássico, redirecione para arquivo rotacionado ou para `logger`.

```cron
15 * * * * /opt/jobs/reconciliar >> /var/log/reconciliar.log 2>&1
```

## Checklist antes de colocar em produção

Antes de confiar em um cron job em Zig, revise:

- a tarefa é idempotente ou tem marcador claro de progresso;
- existe lock ou política de concorrência;
- erro fatal sai com código diferente de zero;
- retry tem limite e backoff;
- chamadas externas têm timeout;
- logs mostram contadores e duração;
- segredos vêm de ambiente/secret manager, não de arquivo commitado;
- o binário aceita `--dry-run` se a rotina altera estado importante;
- o deploy usa usuário sem privilégios excessivos;
- o próximo operador consegue rodar a tarefa manualmente.

Também vale manter um comando de verificação:

```bash
zig build -Doptimize=ReleaseSafe
./zig-out/bin/reconciliar --dry-run
```

`ReleaseSafe` costuma ser um bom padrão para ferramentas operacionais: performance boa, mas com checks que ajudam a pegar bugs. Para jobs extremamente sensíveis a performance, meça antes de mudar para `ReleaseFast`.

## Onde Zig brilha nesse problema

Um scheduler pequeno não precisa de runtime grande. Zig brilha quando você quer entender exatamente o que o binário faz: quais arquivos abre, quanto aloca, quais erros propaga e quando dorme. Isso é especialmente útil em automação de infraestrutura, migração de dados, agentes locais e ferramentas que rodam em muitos ambientes.

Para times que já usam Go em operações, a comparação natural é com CLIs e workers escritos em Go. Go continua excelente para serviços com muita concorrência e ecossistema amplo; há bons materiais de referência em <a href="https://golang.com.br/" target="_blank" rel="noopener noreferrer" onclick="umami.track('portfolio-site-click', { destination: 'golang.com.br' })">Golang Brasil</a>. Zig entra quando binário mínimo, cross-compilation, controle de memória e dependência reduzida são parte do valor.

Comece simples: um binário chamado por cron, com lock, logs e exit code. Se ele crescer, extraia a lógica para funções testáveis, adicione estado transacional e só então considere um daemon. O melhor agendador é aquele que executa a rotina certa, no horário certo, sem virar mais um sistema para operar.
