Expresiones cron desmitificadas: cinco campos, rangos, pasos y la trampa OR de día de semana
Los cinco campos y sus rangos válidos
Toda expresión cron estándar consta de exactamente cinco campos separados por espacios en blanco, leídos de izquierda a derecha: minuto (0–59), hora (0–23), día del mes (1–31), mes (1–12) y día de la semana (0–7). El minuto es siempre el campo más a la izquierda; un error frecuente es leer la línea cron de derecha a izquierda, lo que invierte completamente el significado. El campo de día de la semana acepta tanto 0 como 7 para representar el domingo, una redundancia deliberada heredada de la tradición Unix.
En implementaciones que siguen las convenciones de Vixie cron, el campo del mes también acepta abreviaturas de tres letras en inglés (jan a dec) y el día de la semana acepta sun a sat. Para mayor portabilidad entre entornos, es más seguro usar valores numéricos. Quartz Scheduler (usado en aplicaciones Java) antepone un campo de segundos, y algunos planificadores en la nube como AWS EventBridge añaden un séptimo campo opcional de año — estos formatos extendidos no son intercambiables con el estándar POSIX de cinco campos.
Comodines, listas con coma, rangos con guion y pasos con barra
Cuatro caracteres especiales controlan cómo coincide un campo: el asterisco (*) coincide con cada valor válido del campo — * * * * * se ejecuta cada minuto. La coma (,) crea una lista de valores discretos: 1,3,5 en el campo del mes significa enero, marzo y mayo. El guion (-) define un rango inclusivo: 9-17 en el campo de hora coincide con cada hora de la 9 a la 17 inclusive — útil para horarios de solo días laborables. Estos tres caracteres se pueden combinar: 1-5,0 en el campo de día de la semana coincide de lunes a viernes más el domingo.
La barra (/) introduce un valor de paso. Usada tras un asterisco, */15 en el campo de minutos produce el conjunto {0, 15, 30, 45} — cada múltiplo de 15 desde el mínimo del campo. Usada tras un rango, 0-30/5 produce {0, 5, 10, 15, 20, 25, 30} — cada quinto valor dentro del rango. El paso siempre comienza en el límite izquierdo del rango (o en 0 para *), no en un desplazamiento aleatorio. Por eso */5 en el campo de minutos se dispara en 0, 5, 10 … independientemente de cuándo se inició el demonio cron.
La trampa OR entre día del mes y día de la semana
El comportamiento más sorprendente de cron ocurre cuando ambos el campo de día del mes y el de día de la semana se establecen con valores distintos al comodín. Intuitivamente, 0 2 15 * 5 parece significar 'ejecutar a las 2:00 AM solo los viernes que caigan en el día 15 del mes' — una condición AND. En la práctica, la mayoría de implementaciones de cron, incluidas Vixie cron y dcron, lo tratan como una condición OR: la tarea se ejecuta a las 2:00 AM el día 15 de cada mes, y también a las 2:00 AM cada viernes, independientemente de la fecha. La página de manual cron(5) lo documenta explícitamente: 'si se especifican tanto DOM como DOW, el comando se ejecutará cuando cualquiera de los campos coincida con la hora actual'.
Este comportamiento atrapa a los desarrolladores que intentan expresar condiciones de fecha compuestas. Si realmente necesitas 'el tercer viernes de cada mes', cron no puede expresarlo directamente. La solución idiomática es programar para cada viernes (0 2 * * 5) y añadir una prueba de shell al inicio del comando: [ $(date +\%d) -ge 15 ] && [ $(date +\%d) -le 21 ] && /ruta/al/script. Alternativamente, usa un planificador de nivel superior que soporte nativamente reglas de recurrencia de calendario (RFC 5545 RRULE), como las directivas OnCalendar= de los temporizadores de systemd.
Atajos predefinidos: @reboot, @hourly, @daily, @weekly, @monthly
Vixie cron introdujo un conjunto de atajos con nombre que reemplazan la sintaxis de cinco campos para horarios comunes. @reboot ejecuta la tarea una vez inmediatamente después de que el demonio cron se inicia — útil para tareas ligeras de inicio, aunque para servicios de producción es preferible un sistema init adecuado (unidad systemd, etc.). @hourly equivale a 0 * * * * — se ejecuta en el minuto cero de cada hora. @daily (alias @midnight) equivale a 0 0 * * *; @weekly a 0 0 * * 0 (medianoche del domingo); @monthly a 0 0 1 * * (medianoche del primer día de cada mes); y @yearly (alias @annually) a 0 0 1 1 * (medianoche del 1 de enero).
Estos atajos mejoran la legibilidad de los archivos crontab, pero dependen de que el demonio cron reconozca la sintaxis @ — POSIX no los define, por lo que no están disponibles en algunas implementaciones mínimas o integradas. Al escribir expresiones cron para planificadores en la nube (AWS EventBridge, Google Cloud Scheduler, GitHub Actions), consulta la documentación del proveedor, ya que la mayoría de las plataformas en la nube no implementan los atajos @ y requieren el formato explícito de cinco campos.
Valores de paso y la trampa de 'no cada N desde ahora'
Un malentendido frecuente sobre la sintaxis de paso es tratar */N como 'cada N unidades desde cuando se creó esta tarea'. No lo es. El paso es un filtro módulo sobre valores de tiempo absolutos. */5 en el campo de minutos es una abreviatura del conjunto {x : x mod 5 = 0, 0 ≤ x ≤ 59} = {0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55} — doce posiciones de reloj fijas por hora. El demonio cron compara estas posiciones con el minuto actual del reloj; no tiene memoria de cuándo se registró tu tarea.
Esto produce un problema sutil con 0 */6 * * *. Esta expresión se dispara en las horas 0, 6, 12 y 18 — medianoche, 6 AM, mediodía y 6 PM — exactamente cuatro veces al día. Si añades esta tarea a las 3 PM esperando 'la primera ejecución seis horas después a las 9 PM', te sorprenderá: la próxima ejecución es a las 6 PM (hora 18). Si necesitas una ejecución a las 9 PM, usa la lista explícita 0 9,15,21,3 * * * o cambia la hora de inicio con 0 3/6 * * * — pero nota que la notación INICIO/PASO es una extensión de Vixie cron y puede no estar disponible en todos lados; para máxima portabilidad, usa siempre la lista explícita separada por comas.
Zonas horarias: el error invisible en los horarios cron
Los demonios cron tradicionales ejecutan todas las tareas en la zona horaria del sistema de la máquina donde se ejecutan. En un servidor configurado para UTC — que es el valor predeterminado para la mayoría de las imágenes base de contenedores Docker y muchas VMs en la nube — 0 8 * * 1-5 no significa las 8 AM en tu hora local; significa las 8 AM UTC. Si tu equipo está en UTC+9 (Hora Estándar de Japón), esa tarea se ejecuta a las 5 PM hora local. Las expresiones cron de AWS CloudWatch Events (ahora EventBridge) están documentadas como solo UTC. on: schedule: de GitHub Actions también es UTC.
El horario de verano (DST) introduce dos modos de falla adicionales. Cuando los relojes adelantan (por ejemplo, de 01:59 a 03:00), cualquier tarea cron programada durante la hora saltada simplemente no se ejecuta ese día. Cuando los relojes atrasan (por ejemplo, de 02:59 a 02:00), el mismo minuto aparece dos veces; si la tarea se ejecuta una o dos veces depende de la implementación del demonio. La estrategia más segura en producción es ejecutar los servidores cron permanentemente en UTC y convertir a hora local en la capa de aplicación, o usar un planificador que acepte un parámetro TimeZone explícito, como los temporizadores systemd con TimeZone=, Kubernetes CronJob con .spec.timeZone (añadido en Kubernetes 1.25), o AWS EventBridge Scheduler que acepta nombres de zona horaria IANA.