StackPractices
beginner Por Mathias Paulenko

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.

Temas: devops

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.

flowchart diagram: Tarea recurrente

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:

CampoValores permitidosDescripción
Minuto0-59Minuto de la hora
Hora0-23Hora del día
Día del mes1-31Día del mes
Mes1-12Mes del año
Día de la semana0-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ónHorario
*/5 * * * *Cada 5 minutos
0 * * * *Cada hora
0 0 * * *Todos los días a medianoche
0 9 * * 1Cada lunes a las 9 AM
0 0 1 * *Primer día de cada mes
0 0 * * 0Cada 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 PATH mí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_TZ explí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 +x del script — cron registra Permission denied en 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 flock o max_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

  1. 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
  1. 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()
  1. 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
  1. 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.