Cron: programar tareas en Linux, Python y Node.js
Cómo programar y gestionar tareas recurrentes usando sintaxis cron en Linux, Python, Node.js y Kubernetes.
Visión general
Cron es el planificador de trabajos estándar de Unix: ejecuta comandos en horas, fechas o intervalos fijos definidos por una expresión de cinco campos. Los equipos lo usan para respaldar bases de datos, enviar correos, rotar logs o descargar datos de forma periódica.
El cron del sistema no es la única salida, eso sí. La mayoría de los lenguajes incluyen sus propios planificadores — node-cron, APScheduler, Quartz — y plataformas como Kubernetes y systemd ofrecen primitivas similares. Las secciones de abajo empiezan por el clásico crontab y siguen con cada alternativa.
Cuándo usarlo
Algunas situaciones donde cron es la herramienta correcta:
- Ejecutas tareas periódicas en un servidor — backups, limpiezas, generación de reportes. Consulta Background Jobs para patrones de colas de tareas.
- Necesitas que un trabajo se dispare dentro de tu aplicación y no en el host. Consulta Scheduled Jobs si una función serverless encaja mejor que un daemon.
- Un proceso manual que se olvida con frecuencia merece un script con horario. Consulta Bash Scripting Automation para automatización de scripts.
- Varios nodos deben coordinar el mismo trabajo recurrente. Consulta RabbitMQ Task Queue para coordinación de tareas distribuidas.
Evita el cron del sistema cuando la tarea reacciona a eventos y no al tiempo, necesita precisión de segundos, o corre en una máquina que no siempre está encendida — las colas, los planificadores dentro de la app y los timers de systemd resuelven mejor esos casos.
Solución
Linux (Cron del sistema)
# Editar crontab
crontab -e
# Todos los días a las 3:00 AM
0 3 * * * /usr/local/bin/backup.sh
# Cada 15 minutos
*/15 * * * * /usr/local/bin/check-health.sh
# Cada lunes a las 9:00 AM
0 9 * * 1 /usr/local/bin/weekly-report.sh
# Primer día de cada mes a medianoche
0 0 1 * * /usr/local/bin/monthly-cleanup.sh
Python
import schedule
import time
def job():
print("Running scheduled task...")
# Cada 10 minutos
schedule.every(10).minutes.do(job)
# Todos los días a las 9:30 AM
schedule.every().day.at("09:30").do(job)
# Cada lunes
schedule.every().monday.do(job)
while True:
schedule.run_pending()
time.sleep(1)
JavaScript (Node.js)
const cron = require('node-cron');
// Cada 15 minutos
cron.schedule('*/15 * * * *', () => {
console.log('Running every 15 minutes');
});
// Todos los días a las 3:00 AM
cron.schedule('0 3 * * *', () => {
console.log('Running daily backup');
});
// Cada lunes a las 9:00 AM
cron.schedule('0 9 * * 1', () => {
console.log('Running weekly report');
});
Explicación
Una línea cron tiene cinco campos:
| Campo | Valores permitidos | Descripción |
|---|---|---|
| Minuto | 0-59 | Minuto de la hora |
| Hora | 0-23 | Hora del día |
| Día del mes | 1-31 | Día del mes |
| Mes | 1-12 | Mes del año |
| Día de la semana | 0-7 (0 y 7 = domingo) | Día de la semana |
Caracteres especiales:
*— cualquier valor,— separador de lista de valores-— rango de valores*/n— cada n pasos
Los rangos se combinan con listas: 0 9 * * 1-5 significa días laborables a las 9 AM y 0 9,18 * * * significa 9 AM y 6 PM.
Cron ejecuta los trabajos con un entorno mínimo: el PATH es reducido, tu perfil de shell nunca se carga y el directorio de trabajo es el home del usuario. Por eso un script que funciona en tu terminal falla en silencio a las 3 AM — define PATH y SHELL en el propio crontab, o carga un perfil dentro del trabajo.
Los dos modos de fallo que más duelen en producción son las ejecuciones superpuestas y las zonas horarias. Superposiciones: si un trabajo tarda más que su intervalo, cron arranca otra copia igual — protege los trabajos largos con flock o max_instances (ver Variantes). Zonas horarias: el cron del sistema sigue la hora local de la máquina — un trabajo fijado a las 2:30 AM puede dispararse dos veces o ninguna el día del cambio de horario. CRON_TZ, soportado por cronie y por la mayoría de las librerías, fija el trabajo a una zona horaria concreta en vez de a la del host.
Horarios comunes
| Expresión | Horario |
|---|---|
*/5 * * * * | Cada 5 minutos |
0 * * * * | Cada hora |
0 0 * * * | Todos los días a medianoche |
0 9 * * 1 | Cada lunes a las 9 AM |
0 0 1 * * | Primer día de cada mes |
0 0 * * 0 | Cada domingo a medianoche |
La mayoría de las implementaciones de cron también aceptan atajos con nombre: @reboot (una vez al arrancar), @hourly, @daily (igual que @midnight), @weekly, @monthly y @yearly.
Variantes
Crontab es el punto de partida más fácil, y a menudo el lugar equivocado donde quedarse. Los ejemplos de abajo cubren las alternativas habituales: planificadores dentro de la app para tareas que viven en tu código, Kubernetes CronJobs para cargas en contenedores, y timers de systemd cuando quieres recuperar ejecuciones perdidas e integración con journal en Linux moderno. Las versiones ejecutables de cada ejemplo están en el repositorio complementario.
Java (Quartz Scheduler)
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
public class CronScheduler {
public static void main(String[] args) throws SchedulerException {
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
JobDetail job = JobBuilder.newJob(BackupJob.class)
.withIdentity("backupJob", "group1")
.build();
// Todos los días a las 3:00 AM
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("backupTrigger", "group1")
.withSchedule(CronScheduleBuilder.cronSchedule("0 0 3 * * ?"))
.build();
scheduler.scheduleJob(job, trigger);
scheduler.start();
}
public static class BackupJob implements Job {
@Override
public void execute(JobExecutionContext context) {
System.out.println("Ejecutando backup: " + java.time.LocalDateTime.now());
}
}
}
A diferencia de crontab, las expresiones de Quartz tienen seis campos — los segundos van primero — y ? significa “sin valor específico” en los campos de día.
Kubernetes CronJob
apiVersion: batch/v1
kind: CronJob
metadata:
name: database-backup
spec:
schedule: "0 3 * * *" # Todos los días a las 3:00 AM
jobTemplate:
spec:
template:
spec:
containers:
- name: backup
image: backup-tool:v1.2
command: ["/bin/sh", "-c"]
args: ["pg_dump $DATABASE_URL > /backup/$(date +%Y%m%d).sql"]
envFrom:
- secretRef:
name: db-credentials
restartPolicy: OnFailure
activeDeadlineSeconds: 3600 # Matar después de 1 hora
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 5
concurrencyPolicy: Forbid # Prevenir ejecuciones superpuestas
concurrencyPolicy: Forbid evita la superposición sin código extra, y activeDeadlineSeconds corta cualquier job que se pase del tiempo límite. Si el cluster está caído a la hora programada, startingDeadlineSeconds controla si la ejecución perdida aún se realiza.
Go (robfig/cron)
package main
import (
"fmt"
"github.com/robfig/cron/v3"
)
func main() {
c := cron.New()
// Cada 15 minutos
c.AddFunc("*/15 * * * *", func() {
fmt.Println("Health check ejecutando...")
})
// Todos los días a las 3:00 AM
c.AddFunc("0 3 * * *", func() {
fmt.Println("Backup diario ejecutando...")
})
// Con campo de segundos (opcional)
// c.AddFunc("0 */10 * * * *", func() { ... }) // Cada 10 segundos
c.Start()
select {} // Bloquear para siempre
}
systemd Timer (Alternativa a Cron)
# /etc/systemd/system/backup.service
[Unit]
Description=Database Backup
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
User=backup
Environment=DATABASE_URL=postgres://localhost/mydb
# /etc/systemd/system/backup.timer
[Unit]
Description=Ejecutar backup diario a las 3:00 AM
[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true # Ejecutar trabajos perdidos tras el arranque
[Install]
WantedBy=timers.target
# Habilitar e iniciar el timer
$ sudo systemctl enable backup.timer
$ sudo systemctl start backup.timer
$ systemctl list-timers --all
Dos ventajas sobre cron: Persistent=true recupera las ejecuciones perdidas mientras la máquina estaba apagada, y la salida queda en journalctl -u backup.service en vez de desaparecer.
Prevenir ejecuciones superpuestas con flock
# Entrada crontab con flock para prevenir superposición
0 3 * * * /usr/bin/flock -n /tmp/backup.lock /usr/local/bin/backup.sh
# -n: sin bloqueo, falla si el lock está tomado
# -w 60: esperar hasta 60 segundos por el lock
0 * * * * /usr/bin/flock -w 60 /tmp/cleanup.lock /usr/local/bin/cleanup.sh
Variables de entorno en cron
# Cron tiene un PATH mínimo. Defínelo explícitamente en crontab:
PATH=/usr/local/bin:/usr/bin:/bin
SHELL=/bin/bash
MAILTO=alerts@example.com
# O carga un perfil al inicio de cada trabajo
0 3 * * * . /home/user/.bashrc && /usr/local/bin/backup.sh
Python con APScheduler (Avanzado)
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.cron import CronTrigger
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def backup_job():
logger.info("Iniciando backup de base de datos...")
# Lógica de backup aquí
logger.info("Backup completado.")
def health_check():
logger.info("Health check OK")
scheduler = BackgroundScheduler(timezone="UTC")
# Todos los días a las 3:00 AM UTC
scheduler.add_job(
backup_job,
CronTrigger(hour=3, minute=0),
id="backup",
max_instances=1, # Prevenir superposición
coalesce=True, # Fusionar ejecuciones perdidas
)
# Cada 15 minutos con jitter
scheduler.add_job(
health_check,
CronTrigger(minute="*/15"),
id="health_check",
max_instances=1,
jitter=60, # Retraso aleatorio de hasta 60 segundos
)
scheduler.start()
max_instances=1 cumple el mismo papel que flock en crontab, y coalesce=True colapsa las ejecuciones perdidas en una sola — útil cuando el planificador se reinicia tras un deploy.
Lo que funciona
- Usa rutas absolutas para comandos y scripts; el
PATHmínimo de cron no encontrará tus herramientas. - Redirige la salida a un log o a
/dev/null— si no, cron se la manda por correo al usuario local, donde nadie la leerá. - Define
CRON_TZexplícitamente cuando un trabajo deba dispararse en horario laboral; los cambios de zona del host y el horario de verano pueden desplazarlo sin avisar. - Mantén vivos los planificadores de la app con un gestor de procesos (systemd, PM2) — a diferencia de cron, nada los reinicia si la aplicación muere.
- Añade manejo de errores y alertas — un cron job que falla lo hace en silencio por defecto.
- Valida las expresiones en crontab.guru antes de subirlas a producción.
Errores comunes
- Olvidar el
chmod +xdel script — cron registraPermission denieden syslog y el trabajo sale inmediatamente. - Probar scripts con rutas relativas — pasan en tu shell y se rompen bajo el entorno recortado de cron.
- No controlar las ejecuciones superpuestas — un trabajo lento con intervalo corto genera copias que compiten entre sí; usa
flockomax_instances. - Ignorar el cambio de horario — un trabajo fijado entre las 2:00 y las 3:00 AM hora local puede ejecutarse dos veces o saltarse por completo el día de la transición.
- Apuntar
* * * * *a una API con límite de peticiones — cron no tiene backoff, así que la limitación tiene que vivir dentro del propio trabajo.
Consejos de rendimiento
- Escalona los horarios de los trabajos. Evita ejecutar varios trabajos pesados a la vez:
# Mal: todos a medianoche
0 0 * * * /usr/local/bin/backup.sh
0 0 * * * /usr/local/bin/cleanup.sh
0 0 * * * /usr/local/bin/report.sh
# Bien: escalar por 30 minutos
0 0 * * * /usr/local/bin/backup.sh
30 0 * * * /usr/local/bin/cleanup.sh
0 1 * * * /usr/local/bin/report.sh
- Usa jitter para trabajos distribuidos. Añade un retraso aleatorio para evitar el thundering herd:
import random
import time
def run_with_jitter(max_delay=300):
delay = random.randint(0, max_delay)
time.sleep(delay)
run_job()
- Define timeouts. Evita que trabajos desbocados consuman recursos:
# Matar el trabajo después de 1 hora
0 3 * * * timeout 3600 /usr/local/bin/backup.sh
# Kubernetes CronJob
spec:
jobTemplate:
spec:
activeDeadlineSeconds: 3600
- Limpia los artefactos antiguos. Define políticas de retención:
# Borrar backups de más de 30 días
0 4 * * * find /backup -name "*.sql" -mtime +30 -delete
# Kubernetes CronJob
spec:
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 5
Ver también
- Página man de crontab(5) — la referencia oficial del formato de archivo y las variables de entorno.
- crontab.guru — pega una expresión y obtén los próximos disparos en lenguaje claro.
- Kubernetes CronJob — documentación oficial sobre schedule, concurrencyPolicy y límites de historial.
- node-cron — planificador para Node.js (v3+) con campo de segundos opcional y zona horaria por trabajo.
- APScheduler — planificador Python (3.x) con triggers cron, jitter y manejo de ejecuciones perdidas.
- robfig/cron — planificador Go; la v3 acepta expresiones estándar y un campo de segundos opcional.
- Quartz Scheduler — planificador de trabajos para Java con expresiones cron de seis campos y persistencia de jobs.
- systemd.timer(5) — referencia de unidades timer: sintaxis OnCalendar, Persistent, RandomizedDelaySec.
Preguntas frecuentes
¿Cómo listo los trabajos que ejecutará cron?
crontab -l muestra los del usuario actual; sudo cat /var/spool/cron/crontabs/* muestra los de todos los usuarios.
¿Funcionan los cron jobs dentro de contenedores Docker?
Funcionan mientras el contenedor siga corriendo — si el contenedor para, cron para con él. Muchos equipos prefieren programar en el host o migrar a Kubernetes CronJobs.
¿Qué pasa si un trabajo se pasa de su propio intervalo?
Cron arranca la siguiente copia igualmente — no hay protección integrada. flock o un max_instances a nivel de trabajo evita la acumulación.
¿Se puede correr algo al arrancar y no por horario?
Sí — @reboot se dispara una vez al arrancar. También acepta atajos con nombre: @hourly, @daily, @weekly, @monthly y @yearly.
¿Cron puede ejecutar trabajos más de una vez por minuto?
No por sí solo — el minuto es la unidad más fina que entiende. Los timers de systemd (OnCalendar acepta segundos), el modo de seis campos de node-cron o un planificador dentro de la app bajan de ese nivel.
Mi script funciona en la terminal pero falla bajo cron — ¿por qué?
Casi siempre es el entorno. Cron usa un PATH mínimo y nunca carga tu perfil de shell; usa rutas absolutas, define PATH y SHELL en el crontab, y revisa journalctl o /var/log/syslog para ver el error real.
Recursos Relacionados
APScheduler BackgroundScheduler: Evitar Tareas Superpuestas
Ejecuta tareas tipo cron en Python con APScheduler. Cubre disparadores de intervalo, cron y fecha, almacenes de tareas y planificación en segundo plano.
RecipeEjecutar Jobs Programados con Funciones Serverless
Cómo reemplazar cron jobs con funciones serverless programadas para backups, reportes, limpieza y tareas de mantenimiento periódico.
RecipeTareas en Segundo Plano (Background Jobs)
Descarga trabajo lento de las peticiones HTTP: ejecuta tareas en segundo plano con colas de trabajo, programación cron y workers en Python, JavaScript y Java.
RecipeProgramar y Monitorear DAGs con Apache Airflow
Definí, programá y monitoreá DAGs de Airflow con operators, sensors, XCom, dependencias de tareas y la TaskFlow API.
RecipeConfiguración de Pipelines CI/CD
Configura pipelines CI/CD automatizados para testing, building y deployment de aplicaciones con GitHub Actions y lo que funciona.
RecipeRetry con Backoff Exponencial
Cómo implementar lógica de retry resiliente con backoff exponencial y jitter para fallos transitorios en llamadas de red y APIs.