Los timers de systemd son unidades que disparan otra unidad —normalmente un .service— según una programación temporal. La arquitectura es deliberadamente dual: el fichero .timer declara cuándo se ejecuta algo, y el fichero .service declara qué se ejecuta. Esta separación no es cosmética. Significa que puedes arrancar el servicio manualmente con systemctl start sin tocar el timer, o deshabilitar la programación sin borrar la lógica de ejecución.
¿Por qué systemd en lugar de cron? Cron existe desde los años 70 y funciona bien para lo que hace, pero tiene limitaciones estructurales: sus logs van a syslog mezclados con todo lo demás, no entiende dependencias entre servicios, no tiene forma de decir “ejecuta esto solo si el sistema de ficheros está montado”, y el entorno de ejecución es mínimo y difícil de reproducir. Los timers de systemd heredan todo el modelo de unidades: dependencias declarativas con Requires= y After=, logging capturado automáticamente por journald, control de recursos con CPUQuota= o MemoryMax=, y el mismo systemctl que ya usas para todo lo demás.
Úsalos cuando el trabajo pertenece al sistema —tareas de mantenimiento, backups, rotación de datos, sincronizaciones— especialmente si ese trabajo ya existe como unidad .service o si necesitas visibilidad en el journal. Sigue usando cron cuando gestionas tareas de usuario sin privilegios y systemd --user es más fricción de la que aporta, o cuando heredas infraestructura donde cron está profundamente integrado y el beneficio del cambio no justifica el riesgo.
Lo que se rompe si lo haces mal: si activas el timer pero no el servicio, systemctl start mi-tarea.timer fallará silenciosamente si el .service no existe. Si omites Persistent=true en un timer con OnCalendar= y el sistema estaba apagado cuando tocaba ejecutar, esa ejecución se pierde para siempre —comportamiento radicalmente distinto al de cron con @reboot o anacron. Y si llamas igual al timer y al servicio pero con extensiones distintas, systemd los empareja automáticamente; si los nombres no coinciden, tienes que declararlo explícitamente con Unit= en el .timer.
Ejemplo completo: limpieza nocturna de logs temporales
El escenario: un directorio /var/lib/mi-app/tmp/ acumula ficheros temporales que deben purgarse cada noche a las 2:00, de lunes a viernes. Si el servidor estuvo apagado esa noche, la limpieza debe ejecutarse al arrancar.
# /etc/systemd/system/purga-tmp-app.service [Unit] Description=Purga ficheros temporales de mi-app # Garantiza que el sistema de ficheros local esté montado antes de ejecutar After=local-fs.target [Service] Type=oneshot # Usuario sin privilegios; el directorio debe tener los permisos correspondientes User=mi-app Group=mi-app # ExecStart con ruta absoluta; los timers heredan un entorno limpio de systemd, # no el PATH del shell, así que nunca uses nombres relativos aquí ExecStart=/usr/bin/find /var/lib/mi-app/tmp -type f -mtime +1 -delete # Si el comando falla, que quede registrado claramente en el journal StandardOutput=journal StandardError=journal
# /etc/systemd/system/purga-tmp-app.timer [Unit] Description=Ejecuta purga-tmp-app de lunes a viernes a las 2:00 [Timer] # Sintaxis de calendario: lunes a viernes, 02:00:00 hora local OnCalendar=Mon..Fri 02:00:00 # Si el sistema estaba apagado cuando tocaba, ejecutar al arrancar Persistent=true # Añade hasta 120 segundos de jitter aleatorio para evitar thundering herd # cuando tienes muchos servidores con el mismo timer RandomizedDelaySec=120 [Install] WantedBy=timers.target
# Recarga la configuración para que systemd vea las nuevas unidades systemctl daemon-reload # Habilita e inicia el timer (no el servicio) systemctl enable --now purga-tmp-app.timer # Verifica que el timer está activo y cuándo ejecutará la próxima vez systemctl list-timers purga-tmp-app.timer # Prueba el servicio de forma aislada sin esperar al timer systemctl start purga-tmp-app.service # Revisa la salida de la última ejecución journalctl -u purga-tmp-app.service -n 50
Qué está pasando en cada decisión
Type=oneshot en el servicio es el tipo correcto para trabajos que arrancan, hacen algo y terminan —a diferencia de Type=simple, que asume un proceso que permanece vivo. Sin oneshot, systemd puede considerar la unidad como fallida si el proceso sale demasiado rápido.
OnCalendar=Mon..Fri 02:00:00 usa la sintaxis de calendario normalizado de systemd. El rango Mon..Fri es un shorthand para los cinco días; puedes verificar qué interpreta exactamente systemd con systemd-analyze calendar 'Mon..Fri 02:00:00', que devuelve la próxima fecha calculada y la normalización interna. Compara esto con el equivalente en cron (0 2 * * 1-5): la versión de systemd es más larga pero no requiere recordar que el campo de día de la semana va del 0 al 7 con el domingo representado dos veces.
Persistent=true es la diferencia crítica respecto a cron estándar. Cuando el sistema arranca, systemd compara el timestamp de la última ejecución registrada en /var/lib/systemd/timers/ con el calendario. Si la ejecución programada quedó en el pasado, la dispara inmediatamente. Esto es el equivalente a anacron sin necesitar instalar nada extra.
RandomizedDelaySec=120 introduce un retardo aleatorio de hasta dos minutos. En un entorno con un solo servidor no importa, pero si tienes veinte máquinas con el mismo timer sincronizadas por NTP, sin este parámetro todas golpean la base de datos, el almacenamiento compartido o la API externa exactamente a la misma hora.
Rutas absolutas en ExecStart= son obligatorias porque el entorno del servicio no tiene PATH configurado como el de un usuario interactivo. Si pones find en lugar de /usr/bin/find, el servicio falla con un error poco descriptivo. Puedes añadir Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin en [Service] si tienes muchos comandos y no quieres rutas absolutas en todos, pero la ruta explícita es más resistente a cambios en el sistema.
systemctl list-timers sin argumentos muestra todos los timers activos ordenados por próxima ejecución, con las columnas NEXT, LEFT, LAST, PASSED y UNIT. Es la herramienta de diagnóstico que cron nunca tuvo: de un vistazo sabes qué se va a ejecutar, cuándo fue la última vez, y si hay algún timer que lleva demasiado tiempo sin dispararse.
El emparejamiento automático timer↔service funciona porque ambos ficheros tienen el mismo nombre base (purga-tmp-app). Si el servicio se llamara diferente —por ejemplo porque reutilizas un servicio existente— añades Unit=otro-servicio.service en la sección [Timer] y el emparejamiento automático se ignora.
N° 68