Si alguna vez te despertaron con un “la tarea nocturna no se ejecutó”, ya conoces el problema: programar es aburrido hasta que es lo único que importa. Cron es simple, ubicuo y… sorprendentemente bueno fallando en silencio.
En Debian 13, los timers de systemd son la opción adulta: mejor observabilidad, manejo sensato de dependencias, reejecución tras fallos/marcas perdidas y un modelo de fallos que realmente puedes automatizar. También tienen algunos filos afilados que te lastimarán si los agarras mal.
La decisión: cuándo quedarse con cron o cambiar
Seamos claros: cron está bien para una cantidad sorprendente de cosas. Si tienes un único host, un par de scripts y toleras “se ejecutó en algún momento” como criterio de éxito, cron todavía cumple.
Pero los sistemas de producción no fallan por la “vía feliz”. Fallan durante reinicios, despliegues, incidentes de DNS, discos llenos, montajes NFS colgados y cambios accidentales de entorno. Ahí la simplicidad de cron se convierte en un impuesto operativo.
Usa cron cuando
- Necesitas un calendario y nada más.
- Tienes hosts estables, dependencias mínimas y monitorizas activamente los resultados.
- Mantienes cargas heredadas y una migración añadiría riesgo por poco beneficio.
- Ejecutas en un contenedor donde systemd no es PID 1 y no quieres luchar contra eso.
Usa timers de systemd cuando
- Te importan las ejecuciones perdidas tras un reinicio o un periodo abajo (
Persistent=truees una línea que puede salvar carreras). - Quieres registros en el journal, con metadatos consistentes y filtrado sencillo.
- Necesitas orden de dependencias (after network-online, after mounts, after un servicio de base de datos).
- Necesitas control de recursos: límites de CPU/IO, timeouts e aislamiento.
- Quieres monitorización que no sea “¿recibimos un correo de cron?”
- Quieres prevenir ejecuciones solapadas sin construir montones de ficheros lock frágiles.
Guía de opinión: en Debian 13, las nuevas tareas programadas deberían por defecto usar timers de systemd salvo que tengas una razón clara para no hacerlo. Cron pasa a ser la excepción, no la regla.
Una cita que sigue vigente en operaciones: idea parafraseada
— John Ousterhout y su “la complejidad es enemiga de la fiabilidad”. Los timers no son por complejidad; son por poner la complejidad en el lugar correcto (el gestor de servicios), en vez de dispersarla en scripts.
Datos interesantes y un poco de historia (porque importa)
Entender cómo llegamos aquí te ayuda a predecir modos de fallo. Programar siempre ha sido menos sobre “ejecutar a las 02:00” y más sobre “ejecutar cuando todo está en llamas y aún comportarse bien”.
- Cron se remonta a finales de los setenta, diseñado originalmente para Unix para ejecutar tareas periódicas con sobrecarga mínima. Asume que el host está arriba y el reloj es coherente.
- El cron clásico usa crontabs por usuario y un crontab del sistema global; esa separación es conveniente, pero también fragmenta propiedad y auditabilidad.
- Anacron se introdujo para resolver trabajos perdidos en máquinas que no siempre están encendidas (como portátiles). Los timers de systemd absorben efectivamente esa idea de “recuperar ejecuciones tras el tiempo abajo”.
- Los timers de systemd vienen del modelo “unidades por todas partes”: una ejecución programada es solo un disparador para una unidad de servicio. Por eso obtienes orden de dependencias y registros consistentes.
- El ecosistema cron de Debian históricamente dependía del correo para alertas. Pero los entornos modernos a menudo carecen de un MTA local configurado, así que los fallos quedan silenciosos.
- systemd se integra con cgroups. Eso significa que puedes limitar trabajos descontrolados o proteger el resto del sistema—algo que cron nunca intentó.
- Los timers de systemd soportan aleatorización para evitar la “marea de mil” hosts atacando la misma API a medianoche.
- La sincronización horaria se volvió crítica cuando los sistemas distribuidos se volvieron normales. Tanto cron como timers sufren con relojes mal ajustados, pero systemd ofrece evidencia y herramientas de orden más claras.
- El registro se mudó de ficheros a journals en muchas implantaciones Debian. Los timers se benefician directamente porque stdout/stderr se capturan naturalmente sin trucos.
Broma #1: Cron es como ese compañero que “totalmente envió el correo” — solo sabrás que no fue así cuando alguien se queje.
Modelo mental: qué hace cron vs qué hace systemd
Cron: un programador mínimo
Cron lee entradas de crontab, despierta una vez por minuto y comprueba si debe ejecutar un trabajo. Ejecuta comandos con un entorno escaso, bajo una identidad de usuario, con salida opcionalmente enviada por correo. Eso es básicamente todo.
Sus fortalezas son también sus debilidades:
- Fortaleza: simplicidad predecible. Debilidad: tienes que construir todo lo demás por tu cuenta (bloqueo, reintentos, timeouts, dependencias, registro, alertas).
- Fortaleza: ampliamente entendido. Debilidad: “entendido” a menudo significa “asumido”, y las suposiciones se pudren.
timers de systemd: un disparador más un contrato de servicio
Una unidad timer programa la activación de una unidad service. La unidad service define cómo se ejecuta el trabajo: usuario, entorno, directorio de trabajo, timeouts, control de recursos y qué cuenta como éxito/fallo. Esa separación es la magia.
El resultado es menos conocimiento tribal. Un archivo de unidad systemd se describe por sí mismo de una manera que un snippet de shell al azar en crontab no lo hace.
Diferencias operativas comunes que importan
- Observabilidad: los timers escriben en journald por defecto; cron a menudo no escribe en ninguna parte a menos que redirijas.
- Ejecuciones perdidas: los timers pueden recuperar ejecuciones; cron no, a menos que añadas anacron o lógica personalizada.
- Dependencias: los timers pueden ejecutarse tras montajes/red; cron solo puede “esperar”.
- Solapamientos: systemd puede controlar la concurrencia; cron generará estampidas a menos que bloquees.
- Semántica de fallos: systemd registra códigos de salida, comportamiento de reinicio y límites de velocidad; cron mayormente se encoge de hombros.
Una cosa más: los timers no son “más fiables” por ser mágicos. Son más fiables porque convierten el trabajo en una unidad explícita de ejecución, con comportamientos explícitos. La fiabilidad trata sobre volver lo implícito dolorosamente explícito.
Tareas prácticas (comandos, salidas, decisiones)
Estos son los comandos que realmente ejecuto al migrar o depurar trabajos programados. Cada uno incluye qué estás mirando y la decisión que impulsa.
Tarea 1: Identificar implementaciones de cron y trabajos del sistema impulsados por cron
cr0x@server:~$ dpkg -l | egrep 'cron|anacron|systemd'
ii cron 3.0pl1-... amd64 process scheduling daemon
ii systemd 257-... amd64 system and service manager
Qué significa: Tienes cron clásico instalado. Si también ves paquetes anacron, puede que ya tengas comportamiento parcial de “ejecuciones perdidas” para algunas tareas diarias/semanales.
Decisión: No desinstales cron todavía. Primero inventaría los trabajos y entiende qué depende de él.
Tarea 2: Inventariar entradas de cron de todo el sistema (las que todos olvidan)
cr0x@server:~$ ls -la /etc/cron.d /etc/cron.daily /etc/cron.hourly /etc/cron.weekly /etc/cron.monthly
/etc/cron.d:
total 20
-rw-r--r-- 1 root root 201 Jan 10 10:12 sysstat
-rw-r--r-- 1 root root 349 Jan 10 10:12 logrotate
...
/etc/cron.daily:
total 16
-rwxr-xr-x 1 root root 539 Jan 10 10:12 apt-compat
-rwxr-xr-x 1 root root 377 Jan 10 10:12 man-db
Qué significa: Los paquetes de Debian a menudo instalan trabajos aquí. Algunos ya tienen equivalentes en systemd en versiones más recientes; otros aún dependen de cron.
Decisión: Para trabajos de mantenimiento proporcionados por paquetes, prefiere los valores por defecto de la distribución a menos que tengas fuertes razones. Migrar scripts suministrados por el proveedor rara vez es una ganancia.
Tarea 3: Inventariar crontabs de usuario (donde vive lo raro)
cr0x@server:~$ sudo ls -1 /var/spool/cron/crontabs
postgres
www-data
backup
Qué significa: Estos son crontabs por usuario. A menudo contienen trabajos críticos para el negocio que nadie documentó.
Decisión: Trata cada crontab como código de producción. Exporta y revisa línea por línea antes de cambiar nada.
Tarea 4: Volcar un crontab específico y buscar trampas de entorno
cr0x@server:~$ sudo crontab -u backup -l
MAILTO=ops-alerts
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
15 2 * * * /opt/jobs/backup-nightly.sh
Qué significa: El entorno de cron es explícito aquí (bien), pero MAILTO asume que la entrega de correo funciona (a menudo falso en flotas modernas).
Decisión: Al migrar, incorpora el entorno en la unidad systemd y reemplaza “correo como monitorización” con alertas explícitas.
Tarea 5: Comprobar inventario de timers (qué ya programa systemd)
cr0x@server:~$ systemctl list-timers --all
NEXT LEFT LAST PASSED UNIT ACTIVATES
Mon 2025-12-29 02:15:00 UTC 3h 12min left Sun 2025-12-28 02:15:02 UTC 21h ago backup-nightly.timer backup-nightly.service
Mon 2025-12-29 00:00:00 UTC 1h - - - logrotate.timer logrotate.service
Qué significa: Los timers te muestran NEXT/LAST. Esto responde inmediatamente “¿se ejecutó?” sin grepear algún fichero.
Decisión: Si un trabajo importa, debería ser visible aquí (o en un orquestador), no oculto en un crontab personal.
Tarea 6: Inspeccionar el calendario de un timer y si recupera ejecuciones
cr0x@server:~$ systemctl cat backup-nightly.timer
# /etc/systemd/system/backup-nightly.timer
[Unit]
Description=Nightly backup
[Timer]
OnCalendar=*-*-* 02:15:00
Persistent=true
RandomizedDelaySec=10m
[Install]
WantedBy=timers.target
Qué significa: Persistent=true significa que systemd lo ejecutará lo antes posible tras el arranque si se perdió la hora programada. RandomizedDelaySec distribuye la carga.
Decisión: Para backups, rotación de logs e informes, Persistent=true suele ser correcto. Para trabajos que deben avisar a alguien exactamente a las 02:15, no lo es.
Tarea 7: Inspeccionar la unidad de servicio asociada (aquí vive la fiabilidad)
cr0x@server:~$ systemctl cat backup-nightly.service
# /etc/systemd/system/backup-nightly.service
[Unit]
Description=Nightly backup job
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
User=backup
Group=backup
WorkingDirectory=/opt/jobs
ExecStart=/opt/jobs/backup-nightly.sh
TimeoutStartSec=3h
Nice=10
IOSchedulingClass=best-effort
IOSchedulingPriority=7
Qué significa: Esto es explícito: identidad, directorio de trabajo, timeout. Los trabajos de cron suelen olvidar todo esto y confiar en la suerte.
Decisión: Si tu script depende de montajes, red o directorios específicos, decláralo aquí en vez de codificarlo en lógica frágil dentro del script.
Tarea 8: Validar el parseo del calendario (atrapa energías tipo “31 de feb” antes de producción)
cr0x@server:~$ systemd-analyze calendar "*-*-* 02:15:00"
Original form: *-*-* 02:15:00
Normalized form: *-*-* 02:15:00
Next elapse: Mon 2025-12-29 02:15:00 UTC
(in UTC): Mon 2025-12-29 02:15:00 UTC
From now: 3h 12min left
Qué significa: systemd te dice lo que cree que quisiste decir, y cuándo disparará la próxima vez. Esta es tu primera línea de defensa contra malentendidos de calendario.
Decisión: Si la forma normalizada difiere de tu expectativa, para y corrígela ahora. No “esperes a ver”.
Tarea 9: Comprobar si un timer realmente disparó y qué ocurrió
cr0x@server:~$ journalctl -u backup-nightly.service -n 20 --no-pager
Dec 28 02:15:02 server systemd[1]: Starting backup-nightly.service - Nightly backup job...
Dec 28 02:15:03 server backup-nightly.sh[1142]: snapshot created: tank/backups@2025-12-28
Dec 28 03:01:29 server backup-nightly.sh[1142]: upload complete
Dec 28 03:01:29 server systemd[1]: backup-nightly.service: Deactivated successfully.
Dec 28 03:01:29 server systemd[1]: Finished backup-nightly.service - Nightly backup job.
Qué significa: Obtienes líneas de inicio/fin más la salida del script, vinculadas a un nombre de unidad. Esto es mucho más limpio que “¿a dónde fue ese redirect?”.
Decisión: Si la salida es demasiado ruidosa, arregla el registro del script. No elimines la visibilidad redirigiendo todo a /dev/null.
Tarea 10: Probar si la última ejecución falló (y cómo)
cr0x@server:~$ systemctl status backup-nightly.service --no-pager
● backup-nightly.service - Nightly backup job
Loaded: loaded (/etc/systemd/system/backup-nightly.service; static)
Active: inactive (dead) since Sun 2025-12-28 03:01:29 UTC; 21h ago
Duration: 46min 26.113s
Process: 1142 ExecStart=/opt/jobs/backup-nightly.sh (code=exited, status=0/SUCCESS)
Qué significa: Tienes el código de salida, la duración de ejecución y el comando exacto invocado.
Decisión: Si el estado no es 0/SUCCESS, no adivines. Extrae el código de salida y trátalo: reintentar, alertar o fallar rápido.
Tarea 11: Detectar ejecuciones solapadas (el asesino silencioso de backups y ETL)
cr0x@server:~$ systemctl show backup-nightly.service -p ExecMainStartTimestamp -p ExecMainExitTimestamp -p ActiveEnterTimestamp -p ActiveExitTimestamp
ExecMainStartTimestamp=Sun 2025-12-28 02:15:03 UTC
ExecMainExitTimestamp=Sun 2025-12-28 03:01:29 UTC
ActiveEnterTimestamp=Sun 2025-12-28 02:15:02 UTC
ActiveExitTimestamp=Sun 2025-12-28 03:01:29 UTC
Qué significa: Puedes obtener marcas de tiempo estructuradas para ver si la duración del trabajo se acerca al intervalo programado.
Decisión: Si el tiempo de ejecución está regularmente cerca del intervalo, añade protección de concurrencia y considera ampliar el calendario.
Tarea 12: Comprobar problemas de sincronización horaria que hacen mentir los horarios
cr0x@server:~$ timedatectl
Local time: Mon 2025-12-29 00:03:11 UTC
Universal time: Mon 2025-12-29 00:03:11 UTC
RTC time: Mon 2025-12-29 00:03:11
Time zone: Etc/UTC (UTC, +0000)
System clock synchronized: yes
NTP service: active
RTC in local TZ: no
Qué significa: Si la sincronización del reloj está mal, tanto cron como timers se vuelven caóticos. “Se ejecutó a las 2am” deja de significar algo.
Decisión: Arregla la hora primero. Depurar la programación en un host con reloj desviándose es como depurar almacenamiento en un servidor con cables SATA flojos.
Tarea 13: Verificar una dependencia de montaje (común para backups, informes, ingestión)
cr0x@server:~$ systemctl status mnt-backups.mount --no-pager
● mnt-backups.mount - /mnt/backups
Loaded: loaded (/proc/self/mountinfo; generated)
Active: active (mounted) since Sun 2025-12-28 00:00:41 UTC; 1 day 0h ago
Where: /mnt/backups
What: /dev/mapper/vg0-backups
Qué significa: Tu montaje objetivo existe y está activo. Si no lo está, tu trabajo podría escribir accidentalmente en el filesystem raíz.
Decisión: Añade orden de montaje explícito usando RequiresMountsFor=/mnt/backups en la unidad de servicio.
Tarea 14: Confirmar el entorno que realmente ve tu trabajo bajo systemd
cr0x@server:~$ systemctl show backup-nightly.service -p Environment -p User -p Group -p WorkingDirectory
Environment=
User=backup
Group=backup
WorkingDirectory=/opt/jobs
Qué significa: No se están estableciendo variables de entorno implícitas. Si tu script necesita AWS_REGION o un ajuste de PATH, debes declararlo.
Decisión: Coloca el entorno requerido en un EnvironmentFile= con permisos estrictos, o usa rutas completas en scripts. Prefiero rutas completas para utilidades que no deben cambiar comportamiento.
Tarea 15: Limitación de inicio y ráfagas (por qué tus reintentos “no hicieron nada”)
cr0x@server:~$ systemctl show backup-nightly.service -p StartLimitIntervalUSec -p StartLimitBurst
StartLimitIntervalUSec=10s
StartLimitBurst=5
Qué significa: systemd dejará de intentar después de una ráfaga de fallos dentro del intervalo límite. Esto evita que el flapping se convierta en un auto-DoS.
Decisión: Si usas reintentos, ajusta límites conscientemente. De lo contrario tu plan de “reiniciar al fallo” puede dejar de funcionar en silencio después de cinco fallos rápidos.
Cómo construir un timer + servicio que no te avergüence
Una buena migración trata principalmente de decidir qué es tu trabajo: identidad, dependencias, timeouts, concurrencia y salida. Cron nunca te obligó a decidir esas cosas. systemd lo hará, y ese es el punto.
Empieza con la unidad de servicio
Escribe primero el servicio. El timer debería ser lo aburrido.
- Usa
Type=oneshotpara scripts que se ejecutan y terminan. No pretendas que sea un demonio. - Establece
WorkingDirectory=si tu script espera rutas relativas. Mejor: elimina rutas relativas, pero la realidad es desordenada. - Usa rutas completas en
ExecStart=y dentro de scripts para herramientas críticas. Confiar enPATHes como obtener “funciona en shell, falla a las 2am”. - Establece timeouts. Si el trabajo puede colgar, lo hará. Dale a systemd permiso para matarlo.
- Declara dependencias como montajes y red online. Si el trabajo necesita un montaje, dilo. Si necesita DNS, dilo.
Luego escribe la unidad timer
Los timers son compactos, pero tienen dos campos que definen tu postura de fiabilidad:
Persistent=true: recupera ejecuciones perdidas tras periodos abajo.RandomizedDelaySec=: evita estampidas. Genial para flotas, peligroso para trabajos de “tiempo exacto”.
Ejemplo concreto: migrar un trabajo nocturno de backup
Aquí hay una línea base robusta. No “perfecta”. Pero lo bastante buena para desplegar sin crear un hobby de on-call nuevo.
cr0x@server:~$ sudo tee /etc/systemd/system/backup-nightly.service > /dev/null <<'EOF'
[Unit]
Description=Nightly backup job
Wants=network-online.target
After=network-online.target
RequiresMountsFor=/mnt/backups
[Service]
Type=oneshot
User=backup
Group=backup
WorkingDirectory=/opt/jobs
ExecStart=/opt/jobs/backup-nightly.sh
TimeoutStartSec=3h
KillMode=control-group
Nice=10
IOSchedulingClass=best-effort
IOSchedulingPriority=7
# Basic hardening without breaking scripts:
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/mnt/backups /opt/jobs /var/log
EOF
cr0x@server:~$ sudo tee /etc/systemd/system/backup-nightly.timer > /dev/null <<'EOF'
[Unit]
Description=Nightly backup timer
[Timer]
OnCalendar=*-*-* 02:15:00
Persistent=true
RandomizedDelaySec=10m
AccuracySec=1m
Unit=backup-nightly.service
[Install]
WantedBy=timers.target
EOF
cr0x@server:~$ sudo systemctl daemon-reload
cr0x@server:~$ sudo systemctl enable --now backup-nightly.timer
Created symlink '/etc/systemd/system/timers.target.wants/backup-nightly.timer' → '/etc/systemd/system/backup-nightly.timer'.
Por qué estas elecciones:
RequiresMountsFor=evita el clásico incidente “backup escribió en /mnt/backups que no estaba montado, así que en realidad llenó /”.KillMode=control-groupasegura que los procesos hijos no escapen si el trabajo alcanza el timeout.- El hardening básico reduce el radio de daño. Si tu script necesita más acceso, permítelo explícitamente. Deja que el archivo de unidad sea el contrato.
AccuracySecimpide que systemd intente con demasiada insistencia disparar en un segundo exacto (algo que normalmente no necesitas).
Jitter y “¿por qué se ejecutó a las 02:23?”
Si activas RandomizedDelaySec, el trabajo se ejecutará en algún momento dentro de esa ventana. Esto no es un bug. Es una función de seguridad para la flota. Si finanzas quiere el informe a las 02:15:00, no lo aleatorices. Si el trabajo toca un almacenamiento compartido, aleatorízalo y disfruta durmiendo.
Broma #2: RandomizedDelaySec es la versión educada de “todos dejen de tocar el array de almacenamiento a medianoche”.
Características de fiabilidad que realmente deberías usar
systemd te da muchos controles. Usar todos ellos es cómo creas un archivo de unidad que nadie quiere tocar. Usa los controles que compren fiabilidad por línea de config.
1) Recuperar ejecuciones perdidas: Persistent=true
Si un servidor está abajo a las 02:15, cron se encoge de hombros. Los timers pueden ejecutarse al arrancar. Para backups, rotación de logs, limpieza periódica y tareas de datos “eventualmente consistentes”, los timers persistentes son una victoria directa.
No lo uses cuando tu trabajo debe ejecutarse solo a una hora de reloj específica (por ejemplo, acciones de mercado coordinadas). En esos casos, trata el calendario como un contrato y maneja el tiempo abajo explícitamente.
2) Evitar la estampida: RandomizedDelaySec
Si tienes más de un puñado de hosts, no programes todo a la hora en punto. Tu DNS, tu objeto store, tu base de datos y tu almacenamiento compartido lo notarán a la vez.
La aleatorización es un seguro barato. Cambias exactitud por estabilidad a nivel de sistema.
3) Timeouts: TimeoutStartSec y similares
Cada trabajo que habla con la red puede colgar. Cada trabajo que toca almacenamiento puede colgar. Si tu script no puede colgar, felicitaciones: nunca te has topado con NFS en mal estado.
Elige un timeout basado en tu SLO. Si el trabajo normalmente tarda 10 minutos, un timeout de 3 horas no es “seguro”, es “no notarás que está roto”. Usa presupuestos realistas.
4) Control de concurrencia: una ejecución a la vez
El comportamiento por defecto de cron es “dispara y olvida”, que se convierte en “dispara dos veces y lamento”. Con systemd, puedes diseñar para no solaparse.
Un patrón práctico:
- Haz que el servicio se niegue a iniciar si otra instancia está activa.
- O pon un lock en el script con
flock, pero trata ese lock como parte del contrato (y registra cuando te saltas la ejecución).
systemd no da una bandera de “sin solapamiento” en una línea para timers como la que la gente desearía, pero sí hace los solapamientos visibles y controlables: puedes ver unidades activas, definir timeouts y hacer cumplir una vía de ejecución única.
5) Dependencias: montajes y red no están “probablemente ahí”
Los peores fallos no son “trabajo falló”. Los peores fallos son “trabajo logró hacer lo incorrecto”. Si un backup se ejecuta sin el montaje de backups, puede llenar / y aún salir 0.
Usa RequiresMountsFor= para cualquier ruta que debe estar montada. Usa After=network-online.target solo si realmente necesitas red estable; de lo contrario ralentizas el arranque y complicas el orden.
6) Entorno: hazlo explícito o elimínalo
El entorno de cron es famosamente mínimo. El de systemd también es mínimo, pero de otra manera. Confiar en la inicialización del shell interactivo es cómo obtienes “funciona cuando lo ejecuto”. Esa frase debería disparar una pequeña alarma interna.
Si necesitas secretos o configuración, usa un EnvironmentFile= legible solo por el usuario del servicio, o cárgalos desde un fichero propiedad de root con permisos estrictos. No embebas secretos en archivos de unidad.
7) Control de recursos: evita que los trabajos consuman el host
Aquí es donde systemd supera a cron por mucho. Con cgroups, puedes limitar o priorizar trabajos. Un informe que comprime no debería dejar sin CPU a servicios de producción.
Controles útiles incluyen Nice, IOSchedulingClass y (cuando estés listo) controles de CPU/IO. Empieza pequeño: prioridad y timeouts ya cubren la mayoría de incidentes de trabajos desbocados.
8) Registro: deja de lanzar salida al vacío
Journald facilita capturar salida de forma consistente. Conserva stdout/stderr. Haz que los scripts registren de forma estructurada si es posible (incluso líneas simples “clave=valor” son oro durante incidentes).
Luego monitorízalo. Un trabajo programado sin monitorización es una sorpresa programada.
Guion de diagnóstico rápido
Este es el orden en que compruebo las cosas cuando un trabajo respaldado por timer “no se ejecutó” o “se ejecutó pero no pasó nada”. El objetivo es identificar el cuello de botella en minutos, no en un hilo de Slack que dura hasta la comida.
Primero: ¿systemd pensó que disparó?
- Comprueba
systemctl list-timers --allpor LAST y NEXT. - Comprueba
systemctl status tujob.timerpor estado de activación. - Decisión: Si LAST falta o es antiguo, es programación/habilitación. Si LAST es reciente, es ejecución.
Segundo: ¿arrancó el servicio y qué código de salida devolvió?
- Ejecuta
systemctl status tujob.servicey captura el estado de salida. - Decisión: Salida no cero significa fallo a nivel de trabajo; cero significa “se ejecutó” y tu problema probablemente sea “se ejecutó mal” o “no hizo nada útil”.
Tercero: ¿qué dicen los logs justo alrededor de la ejecución?
- Usa
journalctl -u tujob.servicecon filtros de tiempo o-n. - Decisión: Si los logs están vacíos, puede que estés apuntando a la unidad equivocada, timer equivocado o host equivocado. Si los logs muestran un colgado, revisa dependencias o timeouts.
Cuarto: verificar prerequisitos (montajes, red, DNS, credenciales)
- Comprueba montajes con
systemctl status mnt-*.mountofindmnt. - Comprueba sincronización horaria con
timedatectl. - Decisión: Si los prerequisitos no son estables, arréglalos; no los tapes con reintentos.
Quinto: buscar solapamiento y retropresión
- Comprueba duración del trabajo y frecuencia. Si el trabajo tarda más que su intervalo, tendrás solapamientos o acumulación perpetua.
- Decisión: Si existe solapamiento, aplica política de una sola ejecución y ajusta el calendario o optimiza con seguridad.
Errores comunes: síntomas → causa raíz → solución
Esta sección existe porque estos errores aparecen una y otra vez, especialmente durante migraciones de cron a timers. Si ves el síntoma, no debatas. Salta a la causa raíz.
1) Síntoma: “El timer está habilitado, pero el trabajo nunca se ejecuta”
Causa raíz: La unidad timer está habilitada pero el calendario está mal formado, o no está instalada correctamente en timers.target.
Solución: Valida el calendario y confirma la habilitación.
cr0x@server:~$ systemd-analyze calendar "Mon..Fri *-*-* 02:15:00"
Original form: Mon..Fri *-*-* 02:15:00
Normalized form: Mon..Fri *-*-* 02:15:00
Next elapse: Mon 2025-12-29 02:15:00 UTC
From now: 3h 12min left
cr0x@server:~$ systemctl is-enabled backup-nightly.timer
enabled
2) Síntoma: “Funciona manualmente, pero falla desde el timer”
Causa raíz: Diferencias de entorno: PATH faltante, directorio de trabajo faltante, credenciales ausentes, usuario distinto.
Solución: Haz que la unidad defina el contrato: User=, WorkingDirectory=, rutas completas, EnvironmentFile=.
3) Síntoma: “El trabajo se ejecutó, exit 0, pero no produjo salida / no hizo nada”
Causa raíz: El script comprueba algún estado y sale en silencio, o escribió salida en un sitio inesperado, o el trabajo se ejecutó contra la instancia equivocada (ruta de config equivocada).
Solución: Añade registro explícito. Además asegúrate de que la unidad apunta al script y config correctos. Revisa WorkingDirectory y rutas absolutas.
4) Síntoma: “Se ejecutó tras el reinicio a una hora rara”
Causa raíz: Persistent=true provocó una ejecución de recuperación, posiblemente combinada con RandomizedDelaySec.
Solución: Decide si la recuperación es deseada. Si no, quita Persistent=true. Si sí, comunica ese comportamiento a las partes interesadas y monitorízalo.
5) Síntoma: “Dos instancias se ejecutaron a la vez y corrompieron estado”
Causa raíz: El servicio permite inicios concurrentes, el intervalo del timer es menor que el peor tiempo de ejecución, o una ejecución manual se solapó con la programada.
Solución: Añade bloqueo (nivel script con flock), incrementa el intervalo y establece un timeout realista. Considera también que las operaciones manuales pasen por systemctl start para que sean visibles.
6) Síntoma: “Falló una vez y luego nunca volvió a ejecutarse”
Causa raíz: Se alcanzó el límite de tasa de inicio por fallos rápidos, o el timer está bien pero el servicio falla inmediatamente y se suprime.
Solución: Inspecciona estado y logs; ajusta límites de inicio si vas a reintentar intencionalmente, y arregla la falla subyacente.
7) Síntoma: “Los backups llenaron el filesystem raíz”
Causa raíz: Faltaba el montaje del destino; el script escribió en un directorio que existe en root.
Solución: Usa RequiresMountsFor= y escribe backups en una ruta que solo exista cuando esté montada (o al menos verifica identidad del montaje en el script).
8) Síntoma: “Cron solía enviar por correo fallos; ahora no vemos nada”
Causa raíz: El comportamiento de correo de cron actuaba como alerta accidental. systemd registra en el journal, pero nadie lo vigila.
Solución: Establece monitorización: consulta el estado de unidades, alerta en fallos, alerta en ejecuciones exitosas faltantes y, opcionalmente, reenvía logs del journal a tu canal de logs.
Tres mini-historias del mundo corporativo
Mini-historia 1: El incidente causado por una suposición equivocada
Un equipo migró un trabajo de exportación de base de datos de cron a un timer de systemd. La entrada cron antigua se ejecutaba como postgres. El nuevo servicio se ejecutó como root porque “root puede hacer cualquier cosa”. Esa suposición duró exactamente una semana.
El script de exportación escribía en un directorio propiedad de postgres y usaba un socket local esperando los valores por defecto de ~postgres. Bajo root, aún se conectaba—a veces—pero creó archivos de salida propiedad de root con permisos restrictivos. El trabajo downstream, que aún se ejecutaba como postgres, empezó a fallar al leer las exportaciones. La primera señal no fue una falla limpia; fue un informe obsoleto y un ejecutivo preguntando por qué los números no cambiaban.
On-call revisó la base de datos primero (claro). Luego el almacenamiento (también claro). Horas después, alguien notó que el directorio de exportación tenía archivos con propiedad mezclada y marcas de tiempo que no coincidían con la canalización de informes.
La solución no fue heroica: establecer User=postgres, bloquear WorkingDirectory y dejar de usar comportamiento implícito del directorio home. También añadieron un ReadWritePaths= a nivel de unidad para que el trabajo no pudiera garabatear en otros sitios. El incidente terminó con una línea simple en el postmortem: “Asumimos que root era más seguro. Era solo menos visible.”
Mini-historia 2: La optimización que se volvió en contra
Una empresa tenía una flota de hosts Debian ejecutando compactación de logs y envío cada cinco minutos. Alguien decidió “optimizar el tiempo de arranque” quitando dependencias de network-online.target de servicios programados. Arranque más rápido, menos constraints. En papel, parecía limpio.
En la práctica, el timer disparaba poco después del arranque, antes de que DNS fuera consistente y antes de que una interfaz de red overlay estuviera estable. El trabajo no siempre fallaba ruidosamente. A veces encolaba datos localmente y salía 0. A veces escribía a un destino fallback. A veces se quedaba bloqueado en un connect de socket hasta el timeout por defecto (que era efectivamente “un buen rato”).
Tras una semana, las alarmas de almacenamiento empezaron: las tormentas de arranque (mantenimiento planificado) se correlacionaron con atrasos en logs, que se correlacionaron con una latencia de ingestión que hacía que los dashboards mintieran. La “optimización” no era que el trabajo fuera incorrecto, sino que el sistema ahora se comportaba distinto bajo el churn de arranques.
La solución aburrida fue volver a poner dependencias—pero solo las correctas. Reemplazaron el “esperar por la red” amplio por una dependencia de montaje y una comprobación previa pequeña que validara resolución de nombre para el endpoint específico. También establecieron un TimeoutStartSec sensato y registraron fallos explícitamente. El arranque quedó un poco más lento. Operaciones se volvió dramáticamente más calmado.
Mini-historia 3: La práctica aburrida pero correcta que salvó el día
Un equipo cercano a finanzas ejecutaba informes de conciliación mensuales. Todos odiaban tocarlo porque el script era viejo y las reglas del negocio eran delicadas. Lo que hicieron bien no fue ingenioso: trataron al trabajo programado como un servicio con SLO.
Migraron a un timer de systemd y añadieron dos comprobaciones: (1) alerta si el servicio falla, y (2) alerta si el timer no se ha ejecutado con éxito dentro de la ventana esperada. También fijaron el entorno mediante un EnvironmentFile y usaron rutas completas para cada herramienta invocada.
Un mes, una actualización rutinaria de paquete cambió el comportamiento de una utilidad de parseo (mismo nombre, diferente formato por defecto). El script aún se ejecutó, pero produjo salida que no coincidía con la validación downstream. El trabajo salió non-zero debido a una comprobación estricta, y la unidad pasó a estado failed.
Puesto que el equipo tenía “monitorización aburrida”, la falla se detectó en minutos durante horario laboral. Revirtieron el paquete en ese host, ajustaron el parseo para ser explícito y volvieron a ejecutar el servicio manualmente a través de systemd para que la reejecución quedara registrada y atribuible. Sin pánicos nocturnos, sin números mal explicados, sin reunión de emergencia. Solo una falla limpia y una solución limpia.
Listas de verificación / plan paso a paso
Este es un plan práctico de migración que minimiza sorpresas. Úsalo si migras un trabajo o cien.
Paso 1: Inventariar y clasificar trabajos
- Lista directorios cron del sistema y crontabs de usuario.
- Clasifica cada trabajo: crítico, importante, agradable de tener.
- Decide comportamiento deseado tras tiempo abajo: ¿recuperar o saltar?
Paso 2: Definir el contrato de ejecución para cada trabajo
- ¿Qué usuario debe ejecutarlo?
- ¿Qué directorios deben existir y ser escribibles?
- ¿Qué montajes deben estar presentes?
- ¿Qué condiciones de red se requieren?
- ¿Cuál es el peor tiempo de ejecución aceptable?
- ¿Se permite solapamiento?
Paso 3: Construir la unidad de servicio systemd primero
- Empieza con
Type=oneshot. - Añade
User=,Group=,WorkingDirectory=. - Añade un
TimeoutStartSec=realista. - Añade dependencias de montaje y red solo cuando se requieran.
- Añade hardening mínimo:
NoNewPrivileges,PrivateTmpy rutas de escritura limitadas.
Paso 4: Añadir la unidad timer
- Valida el calendario con
systemd-analyze calendar. - Usa
Persistent=truedonde las ejecuciones perdidas deben recuperarse. - Usa
RandomizedDelaySecen flotas para reducir picos de carga.
Paso 5: Probar como si importara
- Inicia manualmente el servicio:
systemctl start tujob.service. - Revisa estado y logs.
- Simula modos de fallo: montaje ausente, fallo DNS, problema de permisos.
Paso 6: Ejecutar en paralelo brevemente (con cuidado)
- Si es seguro, deja cron deshabilitado pero retenido; o ejecuta el job systemd en modo “dry run” mientras cron sigue siendo primario.
- Nunca ejecutes dos trabajos con estado en paralelo a menos que tengas bloqueo explícito y estés seguro de efectos secundarios seguros.
Paso 7: Cortar y monitorizar
- Deshabilita la entrada de cron una vez que el timer esté habilitado y verificado.
- Alerta en fallos y en ausencia de ejecuciones exitosas.
- Revisa logs tras las primeras ejecuciones reales.
Paso 8: Reducir riesgo con el tiempo
- Mejora scripts para ser idempotentes.
- Añade registro estructurado.
- Haz dependencias explícitas y elimina las accidentales.
Preguntas frecuentes (FAQ)
1) ¿Los timers de systemd siempre son “mejores” que cron?
No. Son mejores cuando necesitas características de fiabilidad y observabilidad. Cron está bien para tareas simples y de bajo impacto. La ganancia no es “modernidad”, es claridad operativa.
2) ¿Cuál es el equivalente en systemd de “ejecutar cada 5 minutos”?
Usa OnUnitActiveSec=5min (monótono) o una expresión OnCalendar como *:0/5 dependiendo de lo que quieras decir por “cada 5 minutos”. Los timers monótonos se basan en la última activación; los de calendario se alinean a los límites de reloj.
3) ¿Cómo evito ejecuciones perdidas tras un reinicio?
Usa Persistent=true en el timer. Luego asegúrate de que tu trabajo sea seguro para ejecutarse tras tiempo abajo (idempotente o consciente del estado).
4) ¿Por qué mi trabajo se ejecuta con un PATH distinto al de mi shell?
Porque las tareas programadas no deberían heredar la configuración interactiva del shell. Arréglalo usando rutas completas en scripts, o estableciendo explícitamente Environment=PATH=... (o un EnvironmentFile) en la unidad de servicio.
5) ¿Pongo la lógica en el timer o en el script?
Pon la programación en el timer y la semántica de ejecución en el service. La lógica de negocio en el script/aplicación. No reimplementes programación y reintentos en shell si systemd puede hacerlo con comportamiento más claro.
6) ¿Qué reemplaza a “cron me enviaba la salida por correo”?
Dos cosas: (1) logs en journald para stdout/stderr, y (2) monitorización que alerta sobre unidades fallidas y ejecuciones faltantes. El correo no es monitorización; es una trampa nostálgica.
7) ¿Puedo ejecutar timers para usuarios no root?
Sí. Puedes usar timers del sistema ejecutando servicios como un User= específico, o unidades systemd a nivel de usuario (puede requerir lingering). Para servidores, las unidades del sistema con User= explícito suelen ser más fáciles de gestionar centralmente.
8) ¿Cómo manejo dependencias de montajes y red de forma limpia?
Usa RequiresMountsFor= para rutas de sistema de ficheros y After=network-online.target/Wants=network-online.target solo cuando el trabajo realmente requiera red estable. Evita dependencias demasiado amplias que ralenticen arranque y oculten comprobaciones reales de readiness.
9) ¿Qué pasa con trabajos que nunca deben solaparse?
Asume que el solapamiento ocurrirá a menos que lo prevengas. Usa semánticas de bloqueo (a menudo flock) y asegúrate de que tu monitorización distinga “omitido por lock” de “ejecutado con éxito”. También ajusta intervalos y timeouts para que el comportamiento sea predecible.
10) ¿Es seguro deshabilitar cron por completo en Debian 13?
A veces. Pero muchos paquetes aún instalan tareas de mantenimiento basadas en cron. Deshabilitar cron globalmente puede crear fallos sutiles. Prefiere migrar tus propios trabajos primero y luego decide si cron debe permanecer instalado para mantenimiento de paquetes.
Conclusión: próximos pasos que merecen la pena
Si gestionas sistemas Debian 13 en producción y te importa no perder trabajo programado, los timers de systemd son la opción por defecto a la que deberías acudir. No eliminan fallos. Hacen los fallos legibles, testeables y monitorizables.
Pasos prácticos siguientes:
- Inventaria trabajos cron y etiqueta los críticos.
- Migra un trabajo crítico a timer + service con usuario explícito, directorio de trabajo, timeout y dependencias de montaje.
- Activa
Persistent=truedonde convenga la recuperación, y añade jitter donde importe la carga de la flota. - Conecta monitorización: alerta en fallos de servicio y en “no hubo ejecución exitosa en la ventana”.
- Solo entonces empieza a borrar entradas de cron. Guarda las definiciones antiguas en control de versiones, no en la papelera.
El mejor sistema de programación es el que hace que “¿se ejecutó?” sea una pregunta de dos minutos con una respuesta aburrida. systemd te lleva allí—si tratas los archivos de unidad como contratos operativos, no como un nuevo lugar para esconder scripts shell.