tutoriales·9 분 읽기

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.

DevToolsHub Team
·
Cron Expressions: Entiende y Escribe Jobs Programados sin Errores

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 unidades
  • a,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.

#cron#scheduling#linux#devops#automatización

관련 기사

JSON Schema: Valida Tus APIs Antes de Desplegarlas
9 min

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.

json-schemaapi