5 Pattern di Espressioni Cron Usati in Produzione Reale
Le espressioni cron sembrano semplici — cinque campi, qualche operatore — ma i team in produzione commettono sempre gli stessi errori: job che saltano silenziosamente certi mesi, esecuzioni sovrapposte che corrompono lo stato condiviso, e centinaia di job che si svegliano tutti nello stesso secondo e travolgono un database. L'espressione è raramente il problema; il problema è non aver pensato ai casi limite.
Questa guida tratta cinque pattern cron che i team di sviluppo usano davvero in produzione, con le motivazioni dietro ciascuno e le trappole da evitare. Usa il Cron Parser di Toova per verificare qualsiasi espressione e vedere i prossimi dieci orari di esecuzione prima di fare il deploy.
Anatomia di un'Espressione Cron
Un'espressione cron standard ha cinque campi separati da spazi:
*/15 * * * *
| | | | |
| | | | +---- giorno della settimana (0-6, 0=Domenica)
| | | +------- mese (1-12)
| | +---------- giorno del mese (1-31)
| +------------- ora (0-23)
+----------------- minuto: ogni 15° (0, 15, 30, 45)
Ogni campo accetta: un valore specifico (5), un carattere jolly (*), un intervallo (1-5), un elenco (1,3,5), o un valore di step (*/15). Alcune implementazioni aggiungono un sesto campo per i secondi; altre (come GitHub Actions) si aspettano esattamente cinque campi. Controlla sempre quale dialetto usa il tuo scheduler prima di scrivere espressioni complesse.
Pattern 1: Ogni 15 Minuti — */15 * * * *
*/15 * * * *
| | | | |
| | | | +---- giorno della settimana (0-6, 0=Domenica)
| | | +------- mese (1-12)
| | +---------- giorno del mese (1-31)
| +------------- ora (0-23)
+----------------- minuto: ogni 15° (0, 15, 30, 45) L'espressione di riferimento per processori di code, heartbeat check e job di polling che devono essere eseguiti frequentemente ma non continuamente. Si avvia a :00, :15, :30 e :45 di ogni ora, ogni giorno.
In un crontab:
# voce crontab — esegui il processore batch ogni 15 minuti
*/15 * * * * /opt/app/bin/process-queue.sh >> /var/log/queue.log 2>&1 In Node.js:
// Node.js con 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);
}); Il problema del thundering herd. Quando esegui centinaia di worker con la stessa espressione */15, si avviano tutti simultaneamente a :00, :15, :30 e :45. Se ogni worker colpisce un database condiviso, ottieni quattro picchi all'ora invece di un carico uniforme. La soluzione è aggiungere un jitter casuale prima di ogni esecuzione:
// Aggiungi jitter per evitare il thundering herd su sistemi distribuiti
cron.schedule('*/15 * * * *', async () => {
const jitterMs = Math.floor(Math.random() * 30_000); // fino a 30s
await sleep(jitterMs);
await processBatch();
}); Fino a 30 secondi di jitter distribuisce il picco in una finestra di 30 secondi mantenendo il job nel ciclo atteso di 15 minuti. Per uno sfasamento più sofisticato, vedi il Pattern 5 (sintassi H di Jenkins).
Usa il Convertitore di Timestamp per tradurre la prossima esecuzione pianificata da Unix epoch a un orario leggibile quando fai il debug dei tempi dei job.
Pattern 2: Orario Lavorativo nei Giorni Feriali — 0 9 * * 1-5
0 9 * * 1-5
| | | | |
| | | | +-- giorno della settimana: lunedì–venerdì (1-5)
| | | +------ mese: ogni mese
| | +---------- giorno del mese: ogni giorno
| +-------------- ora: 9
+------------------ minuto: 0 (esattamente all'ora) Questa espressione si avvia una volta, esattamente alle 9:00, dal lunedì al venerdì. È il pattern standard per: inviare email di digest giornaliero, eseguire report durante l'orario lavorativo, avviare workflow che richiedono revisione umana durante la giornata lavorativa, e fare deploy in staging all'inizio della giornata di sviluppo.
In un Kubernetes CronJob:
# Kubernetes CronJob — email digest nei giorni feriali alle 9 AM 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 La trappola del fuso orario. Gli schedule di Kubernetes CronJob vengono eseguiti nel fuso orario del cluster, che è UTC per default. "9 AM UTC" equivale alle 5 AM Eastern in inverno e alle 4 AM Pacific. I tuoi utenti riceveranno il loro "digest mattutino" nel mezzo della notte. Usa una libreria cron che accetti stringhe di fuso orario IANA:
// Gestisci il fuso orario — "9 AM UTC" raramente corrisponde alle aspettative degli utenti
// Usa una libreria che comprende i fusi orari IANA
import { CronJob } from 'cron';
const job = new CronJob(
'0 9 * * 1-5',
() => sendDailyDigest(),
null,
true,
'America/New_York' // eseguito alle 9 AM Eastern, non alle 9 AM UTC
); Il Convertitore di Fuso Orario ti aiuta a trovare l'equivalente UTC per un dato orario locale in qualsiasi fuso orario IANA.
Numerazione dei giorni della settimana. La definizione cron standard usa 0 o 7 per la domenica. Alcune implementazioni (Quartz, cron4j) usano 1 per il lunedì invece di 0. Controlla la documentazione del tuo scheduler e testa con il Cron Parser prima del deploy.
Pattern 3: Batch Notturno — 0 22 * * *
0 22 * * *
| | | | |
| | | | +-- giorno della settimana: qualsiasi
| | | +------ mese: qualsiasi
| | +---------- giorno del mese: qualsiasi
| +-------------- ora: 22 (22:00)
+------------------- minuto: 0 Viene eseguito una volta al giorno alle 22:00. Il batch notturno è uno dei pattern più comuni nel data engineering: aggrega gli eventi del giorno, ricostruisce gli indici di ricerca, genera report, archivia i log, invia notifiche di fine giornata.
Perché le 22:00 invece della mezzanotte? Due motivi. Primo, le 22:00 danno un margine prima che il giorno si concluda, riducendo la probabilità di raccogliere eventi che arrivano in ritardo. Secondo, eseguire un job pesante a mezzanotte significa competere con la fatturazione di fine mese (Pattern 4) il primo di ogni mese.
I tre modi più comuni per scrivere un job giornaliero:
# Tre modi per scrivere "ogni notte alle 22:00":
0 22 * * * # cron standard
@daily # mezzanotte — NON uguale alle 22:00
0 22 * * * # preferisci sempre l'orario esplicito alle @macro per orari diversi dalla mezzanotte Prevenzione della sovrapposizione. Un job notturno che normalmente dura 45 minuti può occasionalmente durare 2 ore — e se è ancora in esecuzione quando scatta il trigger della notte successiva, entrambi i job modificheranno gli stessi dati. Usa un lock distribuito:
// Controllo di idempotenza — evita la doppia elaborazione se i job si sovrappongono
async function runNightlyBatch() {
const lock = await redis.set(
'nightly-batch-lock',
process.pid,
'EX', 3600, // TTL: 1 ora
'NX' // imposta solo se non esiste
);
if (!lock) {
console.log('Batch già in esecuzione, salto.');
return;
}
try {
await processNightlyData();
} finally {
await redis.del('nightly-batch-lock');
}
}
Il flag Redis NX garantisce che solo un processo possa detenere il lock alla volta. Il TTL impedisce a un job crashato di mantenere il lock per sempre.
Pattern 4: Fatturazione Mensile — 0 0 1 * *
0 0 1 * *
| | | | |
| | | | +-- giorno della settimana: qualsiasi
| | | +------ mese: qualsiasi
| | +---------- giorno del mese: 1 (primo)
| +-------------- ora: 0 (mezzanotte)
+------------------ minuto: 0 Si avvia a mezzanotte il primo giorno di ogni mese. Il pattern classico per: generare fatture mensili, resettare le quote di utilizzo, eseguire la riconciliazione di fine mese, e archiviare i dati del mese precedente.
Le trappole cron per i job mensili:
# Primo del mese a mezzanotte UTC
0 0 1 * *
# ---- TRAPPOLE DA EVITARE ----
# Questo NON esegue "ogni mese" in modo affidabile:
0 0 29-31 * * # i mesi con meno giorni vengono saltati silenziosamente
# Fatturazione trimestrale (gen, apr, lug, ott):
0 0 1 1,4,7,10 *
# Fine mese è complicato — non esiste "ultimo giorno" nel cron standard
# Usa @reboot + logica applicativa, oppure un cron che gira ogni giorno e
# controlla nel codice se oggi è l'ultimo giorno del mese. Fatturazione di fine mese. Se devi fatturare l'ultimo giorno di ogni mese (invece che il primo), non esiste un'espressione cron pulita. La soluzione alternativa:
// Controllo "ultimo giorno del mese" a livello applicativo
cron.schedule('0 23 28-31 * *', async () => {
const today = new Date();
const tomorrow = new Date(today);
tomorrow.setDate(today.getDate() + 1);
// Se domani è il 1°, oggi è l'ultimo giorno del mese
if (tomorrow.getDate() === 1) {
await runEndOfMonthBilling();
}
}); Questo si avvia nei giorni 28–31 di ogni mese, poi controlla nel codice se oggi è effettivamente l'ultimo giorno. Viene eseguito fino a quattro volte inutilmente (nei mesi con meno di 31 giorni), ma il controllo è economico e la logica è chiara.
Per la fatturazione trimestrale, usa l'operatore elenco nel campo del mese: 0 0 1 1,4,7,10 * (gennaio, aprile, luglio, ottobre).
Pattern 5: Schedule con Hash (Sintassi H di Jenkins) — H/30 * * * *
H/30 * * * *
|
+--- H significa che Jenkins sceglie un minuto casuale per ogni job.
H/30 = una volta ogni 30 minuti, a partire da H.
Job diversi ricevono valori H diversi, distribuendo il carico.
Il token H è un'estensione Jenkins-specifica alla sintassi cron. Sostituisce H con un numero pseudo-casuale stabile derivato dal nome del job. H/30 nel campo dei minuti significa "una volta ogni 30 minuti, a partire da un minuto determinato da un hash del nome del job."
In un Jenkinsfile:
// Jenkinsfile — pipeline avviata ogni 30 minuti, sfalsata
pipeline {
triggers {
cron('H/30 * * * *')
}
stages {
stage('Deploy') {
steps {
sh './deploy.sh'
}
}
}
} Perché è importante. Le installazioni Jenkins spesso hanno centinaia di pipeline. Se ogni pipeline usa */30 * * * *, si avviano tutte a :00 e :30, creando enormi picchi di carico sul pool di agent e su tutti i sistemi a valle. Con H/30, ogni job ottiene un offset diverso — uno a :03/:33, un altro a :17/:47 — distribuendo uniformemente il carico nell'arco dell'ora.
GitHub Actions non supporta H. Per sfalsare più workflow Actions, scegli manualmente offset di minuti fissi diversi:
// GitHub Actions — H non è supportato; sfalsare manualmente con offset fissi
// Per sfalsare due job: scegli minuti diversi
name: Job A
on:
schedule:
- cron: '5 * * * *' # si avvia al :05 di ogni ora
---
name: Job B
on:
schedule:
- cron: '35 * * * *' # si avvia al :35 — 30 minuti dopo Job A Questo è meno elegante della sintassi H di Jenkins ma ottiene lo stesso effetto per un piccolo numero di workflow. Per un gran numero di workflow, considera un orchestratore esterno che supporti nativamente lo scheduling distribuito.
Considerazioni Trasversali
L'Idempotenza Non è Negoziabile
Ogni job cron dovrebbe essere sicuro da eseguire due volte. L'infrastruttura si guasta, gli scheduler si riavviano, e i lock distribuiti occasionalmente scadono. Progetta i job in modo che eseguirli una seconda volta — con gli stessi input, allo stesso orario — produca lo stesso risultato. Usa upsert invece di insert, pattern check-before-act e chiavi di idempotenza per le chiamate API esterne.
Alerting sulla Dead Letter
Un job cron che fallisce silenziosamente è peggio di uno che non viene eseguito affatto. Invia un alert a una dead letter queue o a un sistema di monitoraggio quando un job fallisce o non si completa entro la finestra temporale prevista. Strumenti come Sentry Crons, Healthchecks.io e Cronitor aggiungono un heartbeat check: il job invia un ping a un URL quando inizia e quando finisce. Se il ping di fine non arriva entro la scadenza, scatta un alert.
Registra lo Schedule nei Log
Emetti una riga di log strutturata all'inizio e alla fine di ogni esecuzione cron, includendo l'espressione, il timestamp di avvio (non l'orario corrente, che potrebbe differire leggermente a causa del jitter), e la durata. Questo rende semplice correlare un incidente a valle — "il database ha rallentato alle 22:00" — con una specifica esecuzione di un batch job.
Riferimento Rapido
Analizza e valida qualsiasi espressione con il Cron Parser. Converti gli orari pianificati tra fusi orari con il Convertitore di Fuso Orario. Se i tuoi job registrano timestamp Unix, il Convertitore di Timestamp li trasforma istantaneamente in date leggibili.
Per un riferimento completo della sintassi cron con esempi comuni, crontab.guru è il riferimento rapido canonico. Per la sintassi H di Jenkins e la sua specifica completa, consulta la documentazione della sintassi cron di Jenkins Pipeline.