Cron Expressions: Entiende y Escribe Jobs Programados sin Errores
Aprende a leer y escribir cron expressions desde cero, evita los errores más comunes, y domina la sintaxis extendida de herramientas modernas como node-cron o GitHub Actions.
El job que solo falla los martes a las 3 AM
Hay un tipo de bug que todo desarrollador encuentra tarde o temprano: el job programado que debería ejecutarse cada día... pero solo falla ciertos días. O el cron que "debería ser cada hora" pero en realidad corre dos veces. O el que nunca se ejecutó porque el asterisco estaba en la posición equivocada.
Las cron expressions son poderosas y compactas, pero su sintaxis no es intuitiva al principio. En este tutorial vas a entenderlas de una vez.
La anatomía de una cron expression
Una cron expression clásica tiene 5 campos, separados por espacios:
┌─────────── minuto (0-59)
│ ┌───────── hora (0-23)
│ │ ┌─────── día del mes (1-31)
│ │ │ ┌───── mes (1-12 o JAN-DEC)
│ │ │ │ ┌─── día de la semana (0-7, donde 0 y 7 = domingo, o SUN-SAT)
│ │ │ │ │
* * * * *
Cada campo puede contener:
*— cualquier valor (wildcard)- Un número — valor exacto
a-b— rango (de a hasta b)*/n— cada n unidadesa,b,c— lista de valores
Los ejemplos que necesitas memorizar
Empecemos con casos reales en lugar de teoría abstracta:
# Cada minuto
* * * * *
# Cada 5 minutos
*/5 * * * *
# A las 9:00 todos los días
0 9 * * *
# A las 9:30 todos los días laborables (lunes a viernes)
30 9 * * 1-5
# El primer día de cada mes a medianoche
0 0 1 * *
# Cada domingo a las 2:00 AM (para backups, etc.)
0 2 * * 0
# A las 9:00 y a las 18:00 todos los días
0 9,18 * * *
# Cada 15 minutos durante horas de trabajo (9-18h), lunes a viernes
*/15 9-18 * * 1-5
El truco para leer cualquier cron expression
Cuando veas una expression que no entiendes, léela de derecha a izquierda y tradúcela al español:
0 3 * * 1
│ │ │ │ │
│ │ │ │ └─ los lunes (1)
│ │ │ └─── cualquier mes (*)
│ │ └───── cualquier día del mes (*)
│ └─────── a las 3h (3)
└───────── en el minuto 0 (0)
→ "Los lunes, cualquier mes, cualquier día del mes, a las 3:00"
→ "Cada lunes a las 3:00 AM"
Otro ejemplo:
15 10 1,15 * *
│ │ │ │ │
│ │ │ │ └─ cualquier día de la semana (*)
│ │ │ └─── cualquier mes (*)
│ │ └──────── el día 1 y el día 15 del mes
│ └──────────── a las 10h
└────────────── en el minuto 15
→ "El día 1 y el 15 de cada mes a las 10:15"
Los errores más comunes
Error 1: Confundir día de la semana con día del mes
Uno de los bugs más frecuentes: querer ejecutar algo "el primer lunes de cada mes" y escribirlo de forma incorrecta.
# ❌ Esto NO es "el primer lunes del mes"
# Es "el día 1 del mes O cualquier lunes" (OR, no AND)
0 9 1 * 1
# ✅ Para "primer lunes del mes" necesitas lógica en el script
# No se puede expresar directamente en cron estándar
0 9 * * 1
# Y en el script: if [ $(date +%d) -le 7 ]; then ...
Cuando especificas tanto día del mes como día de la semana (ambos distintos de *), cron los trata con OR, no con AND. Es un comportamiento históricamente confuso.
Error 2: El rango horario no funciona como esperas con */n
# ❌ Intención: cada hora entre las 9 y las 18
# Realidad: cada hora que sea múltiplo impar, entre 9 y 18
# Esto ejecuta a las: 9, 11, 13, 15, 17 (cada 2h desde las 9)
0 9-18/2 * * *
# ✅ Cada hora exacta entre las 9 y las 18 (inclusive)
0 9-18 * * *
# ✅ Cada 2 horas entre las 9 y las 18
0 9,11,13,15,17 * * *
Error 3: Olvidar las zonas horarias
El cron daemon usa la zona horaria del sistema. En servidores Linux, esto suele ser UTC. Si tienes usuarios en Madrid (UTC+1 en invierno, UTC+2 en verano):
# Ver zona horaria del sistema
timedatectl
# Configurar cron con zona horaria en algunos sistemas
CRON_TZ=Europe/Madrid
0 9 * * * /usr/bin/mi-script.sh
# En Linux, también puedes ver los logs de ejecución
grep CRON /var/log/syslog
Error 4: 0 y 7 son ambos domingo
En la especificación original de cron, 0 es domingo. Pero algunas implementaciones también aceptan 7 como domingo. Esto causa confusión cuando defines rangos:
# Ambas son equivalentes para "solo domingo":
0 9 * * 0
0 9 * * 7
# ¡Pero esto NO es "lunes a domingo"!
# En muchas implementaciones, este rango es "lunes a domingo" (1-7)
# pero en otras falla o se interpreta diferente
0 9 * * 1-7
# ✅ Más explícito y portable:
0 9 * * 0-6 # domingo (0) a sábado (6)
Sintaxis extendida: el formato de 6 campos
Herramientas modernas como node-cron, cron-parser, o el scheduler de NestJS añaden un sexto campo al inicio: los segundos.
┌──────────── segundo (0-59) ← campo adicional
│ ┌─────────── minuto (0-59)
│ │ ┌───────── hora (0-23)
│ │ │ ┌─────── día del mes (1-31)
│ │ │ │ ┌───── mes (1-12)
│ │ │ │ │ ┌─── día de la semana (0-7)
│ │ │ │ │ │
* * * * * *
// node-cron: formato de 6 campos
const cron = require('node-cron');
// Cada 30 segundos
cron.schedule('*/30 * * * * *', () => {
console.log('Ejecutando cada 30 segundos');
});
// Cada día a las 9:00:00
cron.schedule('0 0 9 * * *', () => {
sendDailyDigest();
});
¡Cuidado! Si copias una expression de 5 campos de una referencia clásica y la usas en node-cron, se desplazarán todos los campos. Lo que era "a las 9:00" (0 9 * * *) en formato de 5 campos se convierte en "en el segundo 0, minuto 9, cualquier hora" en formato de 6 campos.
Cron en GitHub Actions
GitHub Actions tiene su propia sintaxis de schedule, que sí usa el formato estándar de 5 campos pero con restricciones:
# .github/workflows/daily-job.yml
on:
schedule:
# Formato: minuto hora día-mes mes día-semana (UTC)
- cron: '0 8 * * 1-5' # Lunes a viernes a las 8:00 UTC
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Run scheduled task
run: echo "Job ejecutado"
Restricciones importantes en GitHub Actions:
- El mínimo intervalo es cada 5 minutos (
*/5 * * * *) - Los jobs pueden tener retraso de hasta 15-20 minutos en horas pico
- Si el repositorio está inactivo, GitHub puede deshabilitar el schedule automáticamente
- La zona horaria siempre es UTC — no hay forma de especificar otra
# ❌ Demasiado frecuente (GitHub lo ignorará o limitará)
- cron: '* * * * *'
# ✅ Mínimo viable
- cron: '*/5 * * * *'
# ✅ Tipico: una vez al día a las 2 AM UTC (3-4 AM España)
- cron: '0 2 * * *'
Cron en entornos cloud y Kubernetes
AWS EventBridge (CloudWatch Events)
AWS usa la misma sintaxis de 5 campos pero con algunas diferencias:
# AWS usa rate expressions o cron expressions
# En cron expressions de AWS: el año es un sexto campo (al final)
cron(0 9 * * ? *)
# ^ El "?" significa "cualquier valor" para días de semana cuando
# ya especificaste día del mes, o viceversa
Kubernetes CronJob
apiVersion: batch/v1
kind: CronJob
metadata:
name: mi-job
spec:
schedule: "0 3 * * *" # Formato estándar de 5 campos
jobTemplate:
spec:
template:
spec:
containers:
- name: mi-contenedor
image: mi-imagen:latest
restartPolicy: OnFailure
Herramientas para validar tus expressions
Antes de desplegar cualquier cron job, valida la expression. Algunos recursos útiles:
// En Node.js: usa cron-parser para validar
const cronParser = require('cron-parser');
try {
const interval = cronParser.parseExpression('0 9 * * 1-5');
console.log('Próximas ejecuciones:');
for (let i = 0; i < 5; i++) {
console.log(interval.next().toString());
}
} catch (error) {
console.error('Expression inválida:', error.message);
}
También puedes usar nuestra herramienta de conversión de timestamps para verificar que las horas UTC que planeas usar corresponden a las horas locales correctas.
Checklist antes de desplegar un cron job
Antes de poner en producción cualquier job programado, verifica:
- Zona horaria: ¿el servidor usa UTC? ¿Cuál es la hora local equivalente?
- Frecuencia: ¿el job puede solaparse si tarda más de su periodo?
- Idempotencia: ¿qué pasa si se ejecuta dos veces por error?
- Logging: ¿tienes forma de saber si el job se ejecutó y si falló?
- Alertas: ¿recibes notificación si el job no se ejecuta en X horas?
- Timeout: ¿el job tiene un tiempo máximo de ejecución?
# Ejemplo básico de logging en shell
#!/bin/bash
LOG_FILE="/var/log/mi-job.log"
DATE=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$DATE] Iniciando job..." >> $LOG_FILE
# Tu lógica aquí
resultado=$(/usr/bin/mi-script.py 2>&1)
exit_code=$?
if [ $exit_code -eq 0 ]; then
echo "[$DATE] Job completado exitosamente" >> $LOG_FILE
else
echo "[$DATE] ERROR (código $exit_code): $resultado" >> $LOG_FILE
# Enviar alerta (email, webhook, etc.)
fi
Referencia rápida
| Expression | Significado |
|---|---|
* * * * * |
Cada minuto |
*/5 * * * * |
Cada 5 minutos |
0 * * * * |
Cada hora en punto |
0 9 * * * |
Cada día a las 9:00 |
0 9 * * 1-5 |
Lunes a viernes a las 9:00 |
0 9 * * 1 |
Cada lunes a las 9:00 |
0 0 1 * * |
El 1º de cada mes a medianoche |
0 0 1 1 * |
El 1 de enero a medianoche |
0 2 * * 0 |
Cada domingo a las 2:00 AM |
0 9,18 * * * |
Cada día a las 9:00 y a las 18:00 |
0 */6 * * * |
Cada 6 horas (0, 6, 12, 18) |
Con esta base puedes leer cualquier cron expression y escribir las tuyas sin necesidad de adivinar. El truco es siempre validar la expression antes de desplegar, verificar la zona horaria, y preparar el job para ser idempotente.
関連記事
JSON Schema: Valida Tus APIs Antes de Desplegarlas
Aprende a usar JSON Schema para validar datos de entrada en APIs REST, generar documentación automática y prevenir errores en producción antes de que ocurran.
OAuth 2.0 vs JWT: Cuándo Usar Cada Uno (y Cuándo Usarlos Juntos)
Entiende la diferencia real entre OAuth 2.0 y JWT, por qué no son lo mismo, y cómo combinarlos correctamente en autenticación moderna.