Ir para o conteúdo
Toova
Todas as Ferramentas

5 Padrões de Expressões Cron Usados em Produção de Verdade

Toova

Expressões cron parecem simples — cinco campos, alguns operadores — mas times de produção cometem os mesmos erros repetidamente: jobs que silenciosamente são ignorados em certos meses, execuções sobrepostas que corrompem estado compartilhado, e centenas de jobs que acordam ao mesmo segundo e sobrecarregam um banco de dados. A expressão raramente é o problema; o problema é não pensar nos casos extremos.

Este guia cobre cinco padrões cron que times de engenharia realmente usam em produção, com o raciocínio por trás de cada um e as armadilhas a evitar. Use o Cron Parser da Toova para verificar qualquer expressão e ver os próximos dez horários de execução antes de fazer o deploy.

Anatomia de uma Expressão Cron

Uma expressão cron padrão tem cinco campos separados por espaço:

*/15  *  *  *  *
  |   |  |  |  |
  |   |  |  |  +---- dia da semana (0-6, 0=Domingo)
  |   |  |  +------- mês (1-12)
  |   |  +---------- dia do mês (1-31)
  |   +------------- hora (0-23)
  +----------------- minuto: a cada 15 (0, 15, 30, 45)

Cada campo aceita: um valor específico (5), um curinga (*), um intervalo (1-5), uma lista (1,3,5), ou um valor de passo (*/15). Algumas implementações adicionam um sexto campo para segundos; outras (como o GitHub Actions) esperam exatamente cinco. Sempre verifique qual dialeto seu agendador usa antes de escrever expressões complexas.

Padrão 1: A Cada 15 Minutos — */15 * * * *

*/15  *  *  *  *
  |   |  |  |  |
  |   |  |  |  +---- dia da semana (0-6, 0=Domingo)
  |   |  |  +------- mês (1-12)
  |   |  +---------- dia do mês (1-31)
  |   +------------- hora (0-23)
  +----------------- minuto: a cada 15 (0, 15, 30, 45)

A expressão mais usada para processadores de fila, verificações de heartbeat e jobs de polling que precisam rodar frequentemente mas não continuamente. Dispara em :00, :15, :30 e :45 de cada hora, todo dia.

Em um crontab:

# entrada no crontab — executa processador de fila a cada 15 minutos
*/15 * * * * /opt/app/bin/process-queue.sh >> /var/log/queue.log 2>&1

Em Node.js:

// Node.js com node-cron
import cron from 'node-cron';

cron.schedule('*/15 * * * *', async () => {
  const batch = await db.query(
    'SELECT * FROM jobs WHERE status = $1 LIMIT 100',
    ['pending']
  );
  await processBatch(batch);
});

O problema do thundering herd. Quando você executa centenas de workers com a mesma expressão */15, todos disparam simultaneamente em :00, :15, :30 e :45. Se cada worker acessa um banco de dados compartilhado, você terá quatro picos por hora em vez de uma carga uniforme. A solução é adicionar jitter aleatório antes de cada execução:

// Adiciona jitter para evitar thundering herd em sistemas distribuídos
cron.schedule('*/15 * * * *', async () => {
  const jitterMs = Math.floor(Math.random() * 30_000); // até 30s
  await sleep(jitterMs);
  await processBatch();
});

Até 30 segundos de jitter distribui o pico em uma janela de 30 segundos, mantendo o job dentro do ciclo esperado de 15 minutos. Para escalonamento mais sofisticado, veja o Padrão 5 (sintaxe H do Jenkins).

Use o Conversor de Timestamp para traduzir o próximo horário agendado de Unix epoch para um horário legível ao depurar o timing dos jobs.

Padrão 2: Horário Comercial em Dias Úteis — 0 9 * * 1-5

0   9   *   *   1-5
|   |   |   |   |
|   |   |   |   +-- dia da semana: Segunda–Sexta (1-5)
|   |   |   +------ mês: todo mês
|   |   +---------- dia do mês: todo dia
|   +-------------- hora: 9
+------------------ minuto: 0 (exatamente na hora cheia)

Essa expressão dispara uma vez, exatamente às 9h, de segunda a sexta-feira. É o padrão para: enviar emails de digest diário, gerar relatórios em horário comercial, disparar fluxos de trabalho que precisam de revisão humana durante o dia útil e fazer deploy em staging no início do dia de engenharia.

Em um Kubernetes CronJob:

# Kubernetes CronJob — email de digest em dias úteis às 9h UTC
apiVersion: batch/v1
kind: CronJob
metadata:
  name: digest-email
spec:
  schedule: "0 9 * * 1-5"
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: mailer
            image: myapp/mailer:latest
            command: ["/app/send-digest"]
          restartPolicy: OnFailure

Armadilha de fuso horário. Os agendamentos de Kubernetes CronJob rodam no fuso horário do cluster, que por padrão é UTC. "9h UTC" é 6h no horário de Brasília no horário de inverno. Seus usuários vão receber o "digest matinal" de madrugada. Use uma biblioteca cron que aceite strings de fuso horário IANA:

// Atenção ao fuso horário — "9h UTC" raramente é o que os usuários esperam
// Use uma biblioteca que entenda fusos horários IANA
import { CronJob } from 'cron';

const job = new CronJob(
  '0 9 * * 1-5',
  () => sendDailyDigest(),
  null,
  true,
  'America/Sao_Paulo' // executa às 9h no horário de Brasília, não às 9h UTC
);

O Conversor de Fuso Horário ajuda a encontrar o equivalente UTC para um dado horário local em qualquer fuso horário IANA.

Numeração dos dias da semana. A definição padrão do cron usa 0 ou 7 para domingo. Algumas implementações (Quartz, cron4j) usam 1 para segunda-feira em vez de 0. Verifique a documentação do seu agendador e teste com o Cron Parser antes de fazer o deploy.

Padrão 3: Batch Noturno — 0 22 * * *

0   22   *   *   *
|    |   |   |   |
|    |   |   |   +-- dia da semana: qualquer
|    |   |   +------ mês: qualquer
|    |   +---------- dia do mês: qualquer
|    +-------------- hora: 22 (22h)
+------------------- minuto: 0

Executa uma vez por dia às 22h. O batch noturno é um dos padrões mais comuns em engenharia de dados: agregar os eventos do dia, reconstruir índices de busca, gerar relatórios, arquivar logs, enviar notificações de fim de dia.

Por que 22h em vez de meia-noite? Dois motivos. Primeiro, 22h dá uma margem antes da virada do dia, reduzindo a chance de perder eventos que chegam tarde. Segundo, rodar um job pesado à meia-noite significa competir com o faturamento mensal (Padrão 4) no primeiro dia de cada mês.

As três formas mais comuns de escrever um job diário:

# Três formas de escrever "todo dia às 22h":
0 22 * * *    # cron padrão
@daily        # meia-noite — NÃO é o mesmo que 22h
0 22 * * *    # prefira sempre horário explícito ao invés de @macros para horários fora da meia-noite

Prevenção de sobreposição. Um job noturno que normalmente leva 45 minutos pode ocasionalmente demorar 2 horas — e se ainda estiver rodando quando o próximo gatilho disparar, ambos os jobs modificarão os mesmos dados. Use um lock distribuído:

// Verificação de idempotência — evita processamento duplo se o job sobrepuser
async function runNightlyBatch() {
  const lock = await redis.set(
    'nightly-batch-lock',
    process.pid,
    'EX', 3600,   // TTL: 1 hora
    'NX'          // só define se não existir
  );

  if (!lock) {
    console.log('Batch já em execução, pulando.');
    return;
  }

  try {
    await processNightlyData();
  } finally {
    await redis.del('nightly-batch-lock');
  }
}

O flag NX do Redis garante que apenas um processo possa ter o lock de cada vez. O TTL evita que um job com crash mantenha o lock para sempre.

Padrão 4: Faturamento Mensal — 0 0 1 * *

0   0   1   *   *
|   |   |   |   |
|   |   |   |   +-- dia da semana: qualquer
|   |   |   +------ mês: qualquer
|   |   +---------- dia do mês: 1 (primeiro)
|   +-------------- hora: 0 (meia-noite)
+------------------ minuto: 0

Dispara à meia-noite no primeiro dia de cada mês. O padrão clássico para: gerar faturas mensais, resetar cotas de uso, rodar reconciliação de fim de mês e arquivar os dados do mês anterior.

Armadilhas cron para jobs mensais:

# Primeiro do mês à meia-noite UTC
0 0 1 * *

# ---- ARMADILHAS A EVITAR ----

# Isso NÃO executa "todo mês" de forma confiável:
0 0 29-31 * *   # meses com menos dias são silenciosamente ignorados

# Faturamento trimestral (Jan, Abr, Jul, Out):
0 0 1 1,4,7,10 *

# Último dia do mês é complicado — não existe "último dia" no cron padrão
# Use @reboot + lógica na aplicação, ou um cron que roda diariamente e
# verifica via código se hoje é o último dia do mês.

Faturamento no fim do mês. Se você precisa faturar no último dia de cada mês (em vez do primeiro), não existe uma expressão cron limpa para isso. A solução alternativa:

// Verificação de "último dia do mês" na aplicação
cron.schedule('0 23 28-31 * *', async () => {
  const today = new Date();
  const tomorrow = new Date(today);
  tomorrow.setDate(today.getDate() + 1);

  // Se amanhã for dia 1, hoje é o último dia do mês
  if (tomorrow.getDate() === 1) {
    await runEndOfMonthBilling();
  }
});

Dispara nos dias 28–31 de cada mês, depois verifica no código se hoje é realmente o último dia. Roda até quatro vezes desnecessariamente (em meses com menos de 31 dias), mas a verificação é barata e a lógica é clara.

Para faturamento trimestral, use o operador de lista no campo de mês: 0 0 1 1,4,7,10 * (janeiro, abril, julho, outubro).

Padrão 5: Agendamento com Hash (Sintaxe H do Jenkins) — H/30 * * * *

H/30 * * * *
|
+--- H significa que o Jenkins escolhe um minuto aleatório por job.
     H/30 = uma vez a cada 30 minutos, a partir de H.
     Jobs diferentes recebem valores H diferentes, espalhando a carga.

O token H é uma extensão do Jenkins à sintaxe cron. Ele substitui H por um número pseudoaleatório estável derivado do nome do job. H/30 no campo de minuto significa "uma vez a cada 30 minutos, no minuto determinado por um hash do nome do job."

Em um Jenkinsfile:

// Jenkinsfile — pipeline disparado a cada 30 minutos, escalonado
pipeline {
  triggers {
    cron('H/30 * * * *')
  }
  stages {
    stage('Deploy') {
      steps {
        sh './deploy.sh'
      }
    }
  }
}

Por que isso importa. Instalações Jenkins frequentemente têm centenas de pipelines. Se todos usam */30 * * * *, todos disparam em :00 e :30, criando picos massivos de carga no pool de agentes e em qualquer sistema downstream. Com H/30, cada job recebe um offset diferente — um em :03/:33, outro em :17/:47 — distribuindo a carga uniformemente ao longo da hora.

O GitHub Actions não suporta H. Para escalonar múltiplos workflows do Actions, escolha manualmente offsets de minuto diferentes:

// GitHub Actions — H não é suportado; use deslocamentos fixos manualmente
// Para escalonar dois jobs: escolha minutos diferentes
name: Job A
on:
  schedule:
    - cron: '5 * * * *'   # executa aos :05 de cada hora

---
name: Job B
on:
  schedule:
    - cron: '35 * * * *'  # executa aos :35 — 30 minutos após o Job A

É menos elegante que o H do Jenkins, mas alcança o mesmo efeito para um número pequeno de workflows. Para muitos workflows, considere um orquestrador externo que suporte nativamente agendamento distribuído.

Considerações Transversais

Idempotência É Inegociável

Todo job cron deve ser seguro para rodar duas vezes. Infraestruturas falham, agendadores reiniciam e locks distribuídos ocasionalmente expiram. Projete jobs de forma que executá-los uma segunda vez — com os mesmos dados, no mesmo horário — produza o mesmo resultado. Use upserts em vez de inserts, padrões check-before-act, e chaves de idempotência para chamadas a APIs externas.

Alertas para Dead Letter

Um job cron que falha silenciosamente é pior do que um que simplesmente não roda. Envie um alerta para uma fila dead letter ou sistema de monitoramento quando um job falhar ou não completar dentro do tempo esperado. Ferramentas como Sentry Crons, Healthchecks.io e Cronitor adicionam uma verificação de heartbeat: o job faz ping em uma URL quando inicia e quando termina. Se o ping de conclusão não chegar dentro do prazo, um alerta dispara.

Registre o Agendamento

Emita uma linha de log estruturada no início e no fim de cada execução cron, incluindo a expressão, o timestamp de disparo (não o horário atual, que pode diferir ligeiramente por causa do jitter) e a duração. Isso facilita correlacionar um incidente downstream — "o banco de dados ficou lento às 22h" — com uma execução específica de um batch.

Referência Rápida

Analise e valide qualquer expressão com o Cron Parser. Converta horários agendados entre fusos horários com o Conversor de Fuso Horário. Se seus logs de job usam timestamps Unix, o Conversor de Timestamp os transforma em datas legíveis instantaneamente.

Para uma referência abrangente de sintaxe cron e exemplos comuns, o crontab.guru é a referência rápida canônica. Para a sintaxe H do Jenkins e sua especificação completa, consulte a documentação de sintaxe cron do Jenkins Pipeline.