5 Patrones de Expresiones Cron en Producción Real
Las expresiones cron parecen simples — cinco campos, unos pocos operadores — pero los equipos en producción cometen los mismos errores repetidamente: trabajos que se saltan silenciosamente en ciertos meses, ejecuciones superpuestas que corrompen el estado compartido, y cientos de trabajos que se despiertan al mismo segundo y abruman una base de datos. La expresión rara vez es el problema; el problema es no pensar en los casos límite.
Esta guía cubre cinco patrones cron que los equipos de ingeniería realmente usan en producción, con el razonamiento detrás de cada uno y las trampas a evitar. Usa el Analizador Cron de Toova para verificar cualquier expresión y ver los próximos diez tiempos de ejecución antes de desplegar.
Anatomía de una Expresión Cron
Una expresión cron estándar tiene cinco campos separados por espacios:
*/15 * * * *
| | | | |
| | | | +---- día de la semana (0-6, 0=domingo)
| | | +------- mes (1-12)
| | +---------- día del mes (1-31)
| +------------- hora (0-23)
+----------------- minuto: cada 15 (0, 15, 30, 45)
Cada campo acepta: un valor específico (5), un comodín (*), un rango (1-5), una lista (1,3,5), o un valor de paso (*/15). Algunas implementaciones agregan un sexto campo para segundos; otras (como GitHub Actions) esperan exactamente cinco. Siempre verifica qué dialecto usa tu scheduler antes de escribir expresiones complejas.
Patrón 1: Cada 15 Minutos — */15 * * * *
*/15 * * * *
| | | | |
| | | | +---- día de la semana (0-6, 0=domingo)
| | | +------- mes (1-12)
| | +---------- día del mes (1-31)
| +------------- hora (0-23)
+----------------- minuto: cada 15 (0, 15, 30, 45) La expresión predilecta para procesadores de colas, verificaciones de heartbeat y trabajos de polling que necesitan ejecutarse frecuentemente pero no continuamente. Se dispara a las :00, :15, :30 y :45 de cada hora, todos los días.
En un crontab:
# entrada crontab — ejecuta el procesador de cola cada 15 minutos
*/15 * * * * /opt/app/bin/process-queue.sh >> /var/log/queue.log 2>&1 En 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);
}); El problema del thundering herd. Cuando ejecutas cientos de workers con la misma expresión */15, todos se disparan simultáneamente a las :00, :15, :30 y :45. Si cada worker golpea una base de datos compartida, obtienes cuatro picos por hora en lugar de una carga uniforme. La solución es agregar jitter aleatorio antes de cada ejecución:
// Agregar jitter para evitar thundering herd en sistemas distribuidos
cron.schedule('*/15 * * * *', async () => {
const jitterMs = Math.floor(Math.random() * 30_000); // hasta 30s
await sleep(jitterMs);
await processBatch();
}); Hasta 30 segundos de jitter distribuye el pico en una ventana de 30 segundos mientras mantiene el trabajo dentro del ciclo esperado de 15 minutos. Para un escalonamiento más sofisticado, mira el Patrón 5 (sintaxis H de Jenkins).
Usa el Convertidor de Timestamp para traducir tu próxima ejecución programada de epoch Unix a una hora legible cuando depures el timing de los trabajos.
Patrón 2: Horario Laboral en Días Hábiles — 0 9 * * 1-5
0 9 * * 1-5
| | | | |
| | | | +-- día de la semana: lunes–viernes (1-5)
| | | +------ mes: cada mes
| | +---------- día del mes: cada día
| +-------------- hora: 9
+------------------ minuto: 0 (exactamente en punto) Esta expresión se dispara una vez, exactamente a las 9:00 AM, de lunes a viernes. Es el patrón estándar para: enviar correos digest diarios, ejecutar reportes en horario laboral, disparar flujos de trabajo que necesitan revisión humana durante el día de trabajo, y desplegar a staging al inicio del día de ingeniería.
En un Kubernetes CronJob:
# Kubernetes CronJob — correo digest en días hábiles a las 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 trampa de la zona horaria. Los schedules de Kubernetes CronJob corren en la zona horaria del clúster, que es UTC por defecto. "9 AM UTC" es las 5 AM Eastern en invierno y las 4 AM Pacific. Tus usuarios recibirán su "digest matutino" en medio de la noche. Usa una librería de cron que acepte cadenas de zona horaria IANA:
// Manejo de zona horaria — "9 AM UTC" rara vez es lo que esperan los usuarios
// Usar una librería que entienda zonas horarias IANA
import { CronJob } from 'cron';
const job = new CronJob(
'0 9 * * 1-5',
() => sendDailyDigest(),
null,
true,
'America/New_York' // corre a las 9 AM Eastern, no a las 9 AM UTC
); El Convertidor de Zona Horaria te ayuda a encontrar el equivalente UTC para una hora local dada en cualquier zona horaria IANA.
Numeración del día de la semana. La definición cron estándar usa 0 o 7 para el domingo. Algunas implementaciones (Quartz, cron4j) usan 1 para el lunes en lugar de 0. Verifica la documentación de tu scheduler y prueba con el Analizador Cron antes de desplegar.
Patrón 3: Batch Nocturno — 0 22 * * *
0 22 * * *
| | | | |
| | | | +-- día de la semana: cualquiera
| | | +------ mes: cualquiera
| | +---------- día del mes: cualquiera
| +-------------- hora: 22 (10 PM)
+------------------- minuto: 0 Corre una vez por día a las 10 PM. El batch nocturno es uno de los patrones más comunes en ingeniería de datos: agregar los eventos del día, reconstruir índices de búsqueda, generar reportes, archivar logs, enviar notificaciones de fin de día.
¿Por qué las 10 PM en lugar de medianoche? Dos razones. Primero, las 10 PM da un margen antes de que el día cambie, reduciendo la probabilidad de capturar eventos que llegan tarde. Segundo, ejecutar un trabajo pesado a medianoche significa que compite con la facturación de fin de mes (Patrón 4) el primero de cada mes.
Las tres formas más comunes de escribir un trabajo diario:
# Tres formas de escribir "cada noche a las 10 PM":
0 22 * * * # cron estándar
@daily # medianoche — NO es lo mismo que las 10 PM
0 22 * * * # siempre preferir hora explícita sobre @macros para horarios distintos de medianoche Prevención de superposición. Un trabajo nocturno que normalmente tarda 45 minutos puede ocasionalmente tardar 2 horas — y si aún está corriendo cuando se dispara el trigger de la noche siguiente, ambos trabajos modificarán los mismos datos. Usa un bloqueo distribuido:
// Verificación de idempotencia — evita el doble procesamiento si el trabajo se superpone
async function runNightlyBatch() {
const lock = await redis.set(
'nightly-batch-lock',
process.pid,
'EX', 3600, // TTL: 1 hora
'NX' // solo establece si no existe
);
if (!lock) {
console.log('El batch ya está en ejecución, omitiendo.');
return;
}
try {
await processNightlyData();
} finally {
await redis.del('nightly-batch-lock');
}
}
La bandera NX de Redis garantiza que solo un proceso pueda tener el bloqueo a la vez. El TTL evita que un trabajo que crasheó mantenga el bloqueo indefinidamente.
Patrón 4: Facturación Mensual — 0 0 1 * *
0 0 1 * *
| | | | |
| | | | +-- día de la semana: cualquiera
| | | +------ mes: cualquiera
| | +---------- día del mes: 1 (el primero)
| +-------------- hora: 0 (medianoche)
+------------------ minuto: 0 Se dispara a medianoche el primer día de cada mes. El patrón clásico para: generar facturas mensuales, reiniciar cuotas de uso, ejecutar la reconciliación de fin de mes, y archivar los datos del mes anterior.
Trampas de cron para trabajos mensuales:
# Primero del mes a medianoche UTC
0 0 1 * *
# ---- TRAMPAS A EVITAR ----
# Esto NO corre "cada mes" de forma confiable:
0 0 29-31 * * # los meses con menos días se saltan silenciosamente
# Facturación trimestral (ene, abr, jul, oct):
0 0 1 1,4,7,10 *
# El fin de mes es complicado — no hay "último día" en cron estándar
# Usa @reboot + lógica de aplicación, o un cron que corra diariamente y
# verifique si hoy es el último día del mes en el código. Facturación de fin de mes. Si necesitas facturar el último día de cada mes (en lugar del primero), no existe una expresión cron limpia para eso. La solución:
// Verificación "último día del mes" a nivel de aplicación
cron.schedule('0 23 28-31 * *', async () => {
const today = new Date();
const tomorrow = new Date(today);
tomorrow.setDate(today.getDate() + 1);
// Si mañana es el 1, hoy es el último día del mes
if (tomorrow.getDate() === 1) {
await runEndOfMonthBilling();
}
}); Esto se dispara los días 28–31 de cada mes, luego verifica en el código si hoy es realmente el último día. Corre hasta cuatro veces innecesariamente (en meses con menos de 31 días), pero la verificación es barata y la lógica es clara.
Para facturación de inicio de trimestre, usa el operador de lista en el campo del mes: 0 0 1 1,4,7,10 * (enero, abril, julio, octubre).
Patrón 5: Schedule con Hash (Sintaxis H de Jenkins) — H/30 * * * *
H/30 * * * *
|
+--- H significa que Jenkins elige un minuto aleatorio por trabajo.
H/30 = una vez cada 30 minutos, comenzando en H.
Cada trabajo obtiene un valor H diferente, distribuyendo la carga.
El token H es una extensión de Jenkins a la sintaxis cron. Reemplaza H con un número pseudoaleatorio estable derivado del nombre del trabajo. H/30 en el campo de minutos significa "una vez cada 30 minutos, comenzando en un minuto determinado por un hash del nombre del trabajo".
En un Jenkinsfile:
// Jenkinsfile — pipeline disparado cada 30 minutos, escalonado
pipeline {
triggers {
cron('H/30 * * * *')
}
stages {
stage('Deploy') {
steps {
sh './deploy.sh'
}
}
}
} Por qué importa. Las instalaciones de Jenkins a menudo tienen cientos de pipelines. Si cada pipeline usa */30 * * * *, todos se disparan a las :00 y :30, creando enormes picos de carga en el pool de agentes y en los sistemas dependientes. Con H/30, cada trabajo obtiene un offset diferente — uno a las :03/:33, otro a las :17/:47 — distribuyendo la carga uniformemente a lo largo de la hora.
GitHub Actions no soporta H. Para escalonar múltiples workflows de Actions, elige diferentes offsets de minutos fijos manualmente:
// GitHub Actions — H no está soportado; usa schedule con offset manualmente
// Para escalonar dos trabajos: elige minutos fijos diferentes
name: Trabajo A
on:
schedule:
- cron: '5 * * * *' # corre en el :05 de cada hora
---
name: Trabajo B
on:
schedule:
- cron: '35 * * * *' # corre en el :35 — 30 minutos después del Trabajo A Esto es menos elegante que Jenkins H pero logra el mismo efecto para un pequeño número de workflows. Para grandes cantidades de workflows, considera un orquestador externo que soporte nativamente el scheduling distribuido.
Consideraciones Transversales
La Idempotencia Es Innegociable
Cada trabajo cron debe ser seguro de ejecutar dos veces. La infraestructura falla, los schedulers se reinician y los bloqueos distribuidos ocasionalmente expiran. Diseña los trabajos de modo que ejecutarlos una segunda vez — con los mismos inputs, al mismo tiempo — produzca el mismo resultado. Usa upserts en lugar de inserts, patrones de verificar-antes-de-actuar, e idempotency keys para llamadas a APIs externas.
Alertas por Dead Letter
Un trabajo cron que falla silenciosamente es peor que uno que no corre en absoluto. Envía una alerta a una cola de dead letter o sistema de monitoreo cuando un trabajo falla o no completa dentro de su ventana de tiempo esperada. Herramientas como Sentry Crons, Healthchecks.io y Cronitor agregan una verificación de heartbeat: el trabajo hace ping a una URL cuando inicia y cuando termina. Si el ping de finalización no llega dentro del plazo, se dispara una alerta.
Registra el Schedule
Emite una línea de log estructurado al inicio y al final de cada ejecución cron, incluyendo la expresión, el timestamp de disparo (no la hora actual, que puede diferir ligeramente debido al jitter) y la duración. Esto facilita correlacionar un incidente downstream — "la base de datos se ralentizó a las 10 PM" — con una ejecución específica de un batch.
Referencia Rápida
Analiza y valida cualquier expresión con el Analizador Cron. Convierte tiempos programados entre zonas horarias con el Convertidor de Zona Horaria. Si tus logs de trabajo usan timestamps Unix, el Convertidor de Timestamp los convierte en fechas legibles al instante.
Para una referencia completa de sintaxis cron y ejemplos comunes, crontab.guru es la referencia rápida canónica. Para la sintaxis H de Jenkins y su especificación completa, consulta la documentación de sintaxis cron de Jenkins Pipeline.