cron ejecuta comandos en un horario — cada minuto, cada martes a las 3 AM, el primer día del mes — sin que tengas que intervenir. systemd timers hacen lo mismo pero con mejor logging, gestión de dependencias y la capacidad de recuperar ejecuciones perdidas. Con estas dos herramientas, puedes automatizar cualquier tarea recurrente en Linux.
En resumen: cron usa una expresión de cinco campos (minuto hora día mes día_semana) en un archivo crontab para programar comandos. systemd timers usan un archivo .timer emparejado con un archivo .service, ofreciendo programación basada en calendario y monotónica (relativa al arranque) con logging integrado en journald. cron es más simple para tareas rápidas; systemd timers son mejores para cualquier cosa que necesites monitorear en producción.
Fundamentos de cron: los cinco campos
Cada job de cron es una línea en un archivo crontab. El formato:
# ┌───────── 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, 0 y 7 = domingo, o sun-sat)
# │ │ │ │ │
* * * * * comando-a-ejecutar
Cada campo acepta:
| Símbolo | Significado | Ejemplo |
|---|---|---|
* | Cualquier valor | * * * * * = cada minuto |
, | Lista | 1,15,30 * * * * = en los minutos 1, 15 y 30 |
- | Rango | 0 9-17 * * * = cada hora de 9 AM a 5 PM |
/ | Intervalo | */5 * * * * = cada 5 minutos |
Combínalos libremente: 0 */2 * * mon-fri significa "en el minuto 0, cada 2 horas, de lunes a viernes."
Los patrones que más uso:
# Todos los días a las 3:30 AM
30 3 * * * /home/omitsu/scripts/backup-db.sh
# Cada lunes a las 9 AM
0 9 * * 1 /home/omitsu/scripts/weekly-report.sh
# Cada 15 minutos en horario laboral
*/15 9-18 * * mon-fri /home/omitsu/scripts/check-uptime.sh
# Primer día de cada mes a medianoche
0 0 1 * * /home/omitsu/scripts/monthly-cleanup.sh
# Cada 6 horas
0 */6 * * * /home/omitsu/scripts/sync-data.sh
Atajos especiales: ahorra escritura
En vez de escribir los cinco campos, cron soporta atajos con @:
| Atajo | Equivalente | Cuándo se ejecuta |
|---|---|---|
@reboot | — | Una vez al arrancar el sistema |
@hourly | 0 * * * * | Al inicio de cada hora |
@daily | 0 0 * * * | Medianoche de cada día |
@weekly | 0 0 * * 0 | Medianoche del domingo |
@monthly | 0 0 1 * * | Medianoche del día 1 |
@yearly | 0 0 1 1 * | Medianoche del 1 de enero |
@reboot es genuinamente útil — lo uso para iniciar scripts de monitoreo y túneles SSH que no tienen archivo de servicio systemd.
Gestión de cron jobs en la práctica
Comandos de crontab
crontab -e # Editar tu crontab (abre $EDITOR)
crontab -l # Listar entradas actuales
crontab -r # Eliminar todo el crontab (¡cuidado!)
crontab -u omitsu -l # Ver crontab de otro usuario (solo root)
El crontab del sistema está en /etc/crontab y tiene un campo extra — el nombre de usuario — entre los campos de tiempo y el comando:
# /etc/crontab — incluye campo de usuario
*/5 * * * * root /usr/local/bin/system-health-check.sh
Los crontabs por usuario (editados con crontab -e) no necesitan el campo de usuario.
Variables de entorno: la trampa silenciosa
cron se ejecuta con un entorno mínimo — mucho menos que tu shell interactivo. Esto pilla a todo el mundo tarde o temprano. Dentro del crontab puedes definir variables al inicio:
SHELL=/bin/bash
PATH=/usr/local/bin:/usr/bin:/bin
MAILTO=omitsu@32blog.com
# Si MAILTO está vacío, cron no envía correo
# MAILTO=""
30 3 * * * /home/omitsu/scripts/backup.sh
Variables clave:
SHELL: Qué shell ejecuta el comando (por defecto:/bin/sh, no bash)PATH: Dónde buscar ejecutables. El PATH por defecto de cron es extremadamente limitado — usa rutas absolutas o define PATH explícitamenteMAILTO: A dónde enviar stdout/stderr. Cadena vacía = sin correoCRON_TZ: Override de zona horaria para este crontab (no disponible en todas las distros)
Dónde van los logs de cron
Depende de la distribución:
# Debian/Ubuntu
grep CRON /var/log/syslog
# RHEL/CentOS/Fedora
grep CRON /var/log/cron
# Sistemas basados en systemd (journald)
journalctl -u cron.service --since "1 hour ago"
Errores comunes con cron
1. Problemas de PATH. Tu script funciona en la terminal pero falla en cron porque cron no carga .bashrc ni .profile. Usa rutas absolutas para todo — /usr/bin/python3, no python3.
2. Permisos. Los scripts necesitan permiso de ejecución (chmod +x script.sh). También verifica que el usuario de cron pueda leer/escribir los archivos que el script toca.
3. La trampa de "ambos campos de día". Cuando especificas día del mes Y día de la semana, cron ejecuta el job cuando cualquiera de las condiciones es verdadera, no cuando ambas lo son. 0 0 15 * fri se ejecuta el 15 Y cada viernes — no "el 15 si cae en viernes."
4. Ejecuciones superpuestas. Si un job tarda 10 minutos y lo programas cada 5 minutos, se superponen instancias. Usa flock para prevenirlo:
*/5 * * * * flock -n /tmp/my-job.lock /home/omitsu/scripts/slow-job.sh
systemd timers: la alternativa moderna
Un timer de systemd son dos archivos trabajando juntos:
- Un unit
.timer— define cuándo ejecutar - Un unit
.service— define qué ejecutar
Por convención comparten el mismo nombre base: backup.timer dispara backup.service.
Tu primer timer: backup diario
Crea el archivo de servicio — esto define el trabajo real:
# /etc/systemd/system/backup.service
[Unit]
Description=Daily database backup
[Service]
Type=oneshot
ExecStart=/home/omitsu/scripts/backup-db.sh
User=omitsu
Crea el archivo de timer — esto define el horario:
# /etc/systemd/system/backup.timer
[Unit]
Description=Run backup daily at 3:30 AM
[Timer]
OnCalendar=*-*-* 03:30:00
Persistent=true
[Install]
WantedBy=timers.target
Habilita e inicia:
sudo systemctl daemon-reload
sudo systemctl enable --now backup.timer
Listo. Persistent=true significa que si el sistema estaba apagado a las 3:30 AM, el backup se ejecuta apenas el sistema arranca — algo que cron simplemente no puede hacer.
Timers monotónicos vs realtime
Los timers de systemd vienen en dos sabores:
Timers realtime usan OnCalendar= para dispararse en tiempos de reloj específicos (como cron):
[Timer]
OnCalendar=Mon..Fri *-*-* 09:00:00
Timers monotónicos se disparan relativo a un evento — tiempo de arranque, activación del timer, o última finalización del servicio:
[Timer]
# 5 minutos después del arranque
OnBootSec=5min
# 1 hora después de activar el timer
OnActiveSec=1h
# 30 minutos después de que el servicio terminó
OnUnitInactiveSec=30min
Los timers monotónicos resuelven el problema de "cada 35 minutos" que cron no puede manejar. OnUnitInactiveSec=35min significa "35 minutos después de que terminó la última ejecución" — sin superposición, sin cálculos de calendario.
Puedes combinar triggers monotónicos y realtime en el mismo archivo de timer. Por ejemplo: OnBootSec=5min más OnCalendar=daily significa "ejecutar 5 minutos después del arranque Y a medianoche todos los días."
Sintaxis de OnCalendar y horarios reales
El formato de OnCalendar es más expresivo que los cinco campos de cron:
DíaDeLaSemana Año-Mes-Día Hora:Minuto:Segundo
Cada parte es opcional. Correspondencia con cron:
| Equivalente cron | Expresión OnCalendar | Significado |
|---|---|---|
0 * * * * | *-*-* *:00:00 o hourly | Cada hora |
0 0 * * * | *-*-* 00:00:00 o daily | Cada día a medianoche |
0 0 * * 0 | weekly o Sun *-*-* 00:00:00 | Cada domingo |
0 0 1 * * | monthly o *-*-01 00:00:00 | Primer día del mes |
0 9 * * 1-5 | Mon..Fri *-*-* 09:00:00 | Días laborales a las 9 AM |
*/15 * * * * | *-*-* *:00/15:00 | Cada 15 minutos |
0 0 1 1 * | yearly o *-01-01 00:00:00 | 1 de enero |
Ejemplos más complejos:
# Días laborales a las 9 AM y 6 PM
OnCalendar=Mon..Fri *-*-* 09,18:00:00
# Cada trimestre — primer día de ene, abr, jul, oct
OnCalendar=*-01,04,07,10-01 00:00:00
# ¿Último día del mes? systemd no soporta "último día" nativamente.
# Workaround: ejecutar del 28 al 31 y verificar dentro del script.
Valida con systemd-analyze
Antes de desplegar un timer, verifica tu expresión:
$ systemd-analyze calendar "Mon..Fri *-*-* 09:00:00"
Original form: Mon..Fri *-*-* 09:00:00
Normalized form: Mon..Fri *-*-* 09:00:00
Next elapse: Mon 2026-03-23 09:00:00 JST
(in UTC): Mon 2026-03-23 00:00:00 UTC
From now: 14h left
$ systemd-analyze calendar "hourly" --iterations=5
Original form: hourly
Normalized form: *-*-* *:00:00
Next elapse: Mon 2026-03-23 20:00:00 JST
Iter. 2: Mon 2026-03-23 21:00:00 JST
Iter. 3: Mon 2026-03-23 22:00:00 JST
Iter. 4: Mon 2026-03-23 23:00:00 JST
Iter. 5: Tue 2026-03-24 00:00:00 JST
Esta es una de las mejores funciones de los timers de systemd — puedes verificar exactamente cuándo se disparará un timer antes de habilitarlo.
Tip: pasa siempre --iterations=5 al testear. Ver las próximas cinco ejecuciones detecta errores off-by-one y sorpresas de zona horaria que un solo "next elapse" no revelaría.
cron vs systemd timers: cuándo usar cada uno
| Característica | cron | systemd timer |
|---|---|---|
| Complejidad de setup | 1 línea en crontab | 2 archivos (.timer + .service) |
| Logging | syslog/correo | journald (filtrado por unidad) |
| Ejecuciones perdidas | Se pierden | Persistent=true las recupera |
| Relativo al arranque | Solo @reboot | OnBootSec, OnStartupSec |
| Basado en intervalo | No es posible | OnUnitInactiveSec |
| Prevención de overlap | Manual (flock) | Type=oneshot integrado |
| Límites de recursos | Ninguno | Soporte completo de cgroups |
| Dependencias | Ninguna | After=, Requires=, Wants= |
| Jitter aleatorio | RANDOM_DELAY (limitado) | RandomizedDelaySec |
| Timers de usuario | crontab -e | ~/.config/systemd/user/ |
Usa cron cuando:
- Necesitas algo que se configure en 30 segundos
- El sistema no usa systemd (contenedores, distros antiguas, macOS)
- Es una automatización rápida que no necesita monitoreo
Usa systemd timers cuando:
- Quieres logs que puedas buscar (
journalctl -u backup.service) - El job debe ejecutarse aunque el sistema estuviera apagado
- Necesitas límites de recursos (que un backup desbocado no devore toda la RAM)
- El job depende de otros servicios (red, base de datos, puntos de montaje)
- Necesitas programación basada en intervalos ("cada 30 min después de la última ejecución")
En 32blog uso cron para verificaciones rápidas (monitoreo de uptime, alertas de expiración de certificados) y systemd timers para cualquier cosa que toque la base de datos o el pipeline de deploy. El factor decisivo es simple: "¿Necesitaré depurar esto a las 2 AM?" Si sí, systemd timers — porque journalctl -u my-service.service es infinitamente mejor que excavar en syslog.
Patrones de producción y debugging
Patrón 1: Prevenir superposición con systemd
Con cron necesitas flock. Con systemd, Type=oneshot ya previene la superposición — systemd no inicia una nueva instancia mientras la anterior sigue corriendo. Pero si necesitas más control:
[Service]
Type=oneshot
# Matar el job si tarda más de 1 hora
TimeoutStartSec=3600
# Política de restart para fallos transitorios
Restart=on-failure
RestartSec=60
Patrón 2: Notificaciones de fallo
cron envía correo en caso de fallo (si el correo está configurado). systemd te da más opciones:
# /etc/systemd/system/backup.service
[Unit]
Description=Daily database backup
OnFailure=notify-failure@%n.service
Crea un servicio genérico de notificación:
# /etc/systemd/system/notify-failure@.service
[Unit]
Description=Send failure notification for %i
[Service]
Type=oneshot
ExecStart=/home/omitsu/scripts/notify-slack.sh "Unit %i failed on %H"
User=omitsu
Ahora cualquier servicio puede enviar una notificación a Slack en caso de fallo agregando OnFailure=notify-failure@%n.service.
Patrón 3: Delay aleatorio para sistemas distribuidos
Cuando 50 servidores ejecutan el mismo backup a las 3:00 AM exactas, tu servidor de backup colapsa. Agrega jitter:
[Timer]
OnCalendar=*-*-* 03:00:00
RandomizedDelaySec=1800
# Se ejecuta aleatoriamente entre 3:00 y 3:30 AM
Patrón 4: Timers de usuario (sin root)
Puedes crear timers sin acceso root usando systemd a nivel de usuario:
mkdir -p ~/.config/systemd/user/
# ~/.config/systemd/user/sync-notes.timer
[Unit]
Description=Sync notes every 30 minutes
[Timer]
OnUnitInactiveSec=30min
OnBootSec=5min
[Install]
WantedBy=timers.target
# ~/.config/systemd/user/sync-notes.service
[Unit]
Description=Sync notes with remote
[Service]
Type=oneshot
ExecStart=%h/scripts/sync-notes.sh
systemctl --user daemon-reload
systemctl --user enable --now sync-notes.timer
systemctl --user list-timers
Checklist de debugging
Cuando un job programado no se ejecuta:
Para cron:
- Verificar que cron está corriendo:
systemctl status cron - Verificar el crontab:
crontab -l - Revisar syslog:
grep CRON /var/log/syslog | tail -20 - Ejecutar el script manualmente:
/path/to/script.sh - Verificar PATH — el PATH de cron es mínimo
- Verificar permisos del script y archivos que toca
Para systemd timers:
- Estado del timer:
systemctl status backup.timer - Próxima ejecución:
systemctl list-timers backup.timer - Logs del servicio:
journalctl -u backup.service --since "1 hour ago" - Ejecutar el servicio manualmente:
systemctl start backup.service - Verificar sintaxis:
systemd-analyze verify backup.timer
FAQ
¿Cómo listo todos los jobs programados en un sistema?
Para cron, revisa múltiples ubicaciones: crontab -l para el usuario actual, sudo crontab -l -u root para root, y ls /etc/cron.d/ /etc/cron.daily/ /etc/cron.hourly/ para jobs del sistema. Para systemd timers: systemctl list-timers --all muestra cada timer con sus próximas y últimas ejecuciones.
¿Puedo ejecutar un cron job cada 30 segundos?
La granularidad mínima de cron es 1 minuto. El workaround clásico son dos entradas:
* * * * * /path/to/script.sh
* * * * * sleep 30 && /path/to/script.sh
Con systemd, usa un timer monotónico: OnUnitInactiveSec=30s. Pero si necesitas programación por debajo del minuto, considera si un daemon de larga ejecución sería más apropiado.
¿Cómo migro un cron job a systemd timer?
Crea dos archivos: un .service con el comando (ExecStart= reemplaza el comando del cron), y un .timer con OnCalendar= correspondiente a tu horario cron. Agrega Persistent=true si quieres recuperar ejecuciones perdidas. Ejecuta systemd-analyze calendar para verificar que el horario coincide, luego systemctl enable --now tu.timer.
¿Qué pasa con los cron jobs si el sistema está apagado?
Se omiten — cron no tiene concepto de ponerse al día. Si tu backup diario estaba programado para las 3 AM y el servidor estuvo caído de 2 AM a 5 AM, el backup no se ejecuta ese día. Los timers de systemd con Persistent=true resuelven esto ejecutando el job apenas el sistema vuelve.
¿Puedo usar cron dentro de contenedores Docker?
Técnicamente sí, pero generalmente es el enfoque equivocado. Los contenedores están diseñados para ejecutar un solo proceso. En su lugar, usa el cron o systemd timer del host para ejecutar docker exec container_name command, o usa la programación de orquestación de contenedores (Kubernetes CronJobs, ECS Scheduled Tasks).
¿Cómo configuro la zona horaria de un cron job?
Algunas implementaciones de cron soportan CRON_TZ=America/New_York al inicio del crontab. Si la tuya no, envuelve el comando: TZ=America/New_York date dentro de tu script. Para timers de systemd, configura Environment=TZ=America/New_York en el archivo de servicio, o usa timedatectl set-timezone a nivel de sistema.
¿Qué es AccuracySec en timers de systemd?
AccuracySec= controla la precisión del disparo del timer. El default es 1 minuto — un timer configurado para 03:00:00 podría dispararse a las 03:00:45. Esto es intencional: systemd agrupa múltiples timers en menos wake-ups para ahorrar energía. Configura AccuracySec=1s si necesitas precisión, pero el default funciona bien para la mayoría de jobs.
¿Cómo evito que un cron job se ejecute si la instancia anterior sigue corriendo?
Usa flock: */5 * * * * flock -n /tmp/my-job.lock /path/to/job.sh. El flag -n hace que flock salga inmediatamente (en vez de esperar) si el lock está tomado. Con systemd, Type=oneshot ya previene la superposición — systemd no inicia una nueva instancia mientras el servicio está activo.
Conclusión
cron y systemd timers resuelven el mismo problema — ejecutar comandos en un horario — pero son herramientas diferentes para situaciones diferentes.
cron es el setup de 30 segundos: edita el crontab, pega una línea, listo. Lleva haciendo esto desde los años 70 y funciona en todos lados. La sintaxis de cinco campos vale la pena memorizarla porque la encontrarás en configs de CI/CD, Kubernetes CronJobs, GitHub Actions y schedulers de cloud en todas partes.
systemd timers son la opción de grado producción: logging apropiado, recuperación de ejecuciones perdidas, límites de recursos, orden de dependencias y programación basada en intervalos. El setup de dos archivos (.timer + .service) se siente más pesado, pero systemctl list-timers y journalctl -u my.service hacen el debugging infinitamente más fácil que buscar en syslog.
Empieza con cron cuando necesites algo corriendo en 30 segundos. Gradúa a systemd timers cuando necesites confiabilidad, observabilidad, o cualquier cosa más allá de fire-and-forget.
Para más automatización CLI, revisa make para task runners, xargs para ejecución paralela de comandos, y el mapa completo de herramientas CLI para una visión general.