StackPractices
beginner By Mathias Paulenko

Cron Jobs: Schedule Tasks in Linux, Python & Node.js

How to schedule and manage recurring tasks using cron syntax across Linux, Python, Node.js, and Kubernetes.

Topics: devops

Overview

Cron is the standard Unix job scheduler: it runs commands at fixed times, dates, or intervals defined by a five-field expression. Teams reach for it to back up databases, send emails, rotate logs, or pull data on a schedule.

System cron isn’t the only option, though. Most languages ship their own schedulers — node-cron, APScheduler, Quartz — and platforms like Kubernetes and systemd offer cron-like primitives of their own. The sections below start with the classic crontab and then move through each alternative.

When to Use

Some situations where cron is the right tool:

  • You run periodic tasks on a server — backups, cleanups, report generation. See Background Jobs for task queue patterns.
  • You need a job to fire inside your application rather than on the host. See Scheduled Jobs when a serverless function fits better than a daemon.
  • A manual process keeps getting forgotten and deserves a script with a schedule. See Bash Scripting Automation for script automation.
  • Several nodes must coordinate the same recurring work. See RabbitMQ Task Queue for distributed task coordination.

Skip system cron when the task reacts to events rather than time, needs sub-minute precision, or runs on a machine that isn’t always on — queues, in-app schedulers, and systemd timers handle those cases better.

flowchart diagram: Recurring task

Solution

Linux (System Cron)

# Edit crontab
crontab -e

# Every day at 3:00 AM
0 3 * * * /usr/local/bin/backup.sh

# Every 15 minutes
*/15 * * * * /usr/local/bin/check-health.sh

# Every Monday at 9:00 AM
0 9 * * 1 /usr/local/bin/weekly-report.sh

# Every first day of the month at midnight
0 0 1 * * /usr/local/bin/monthly-cleanup.sh

Python

import schedule
import time

def job():
    print("Running scheduled task...")

# Every 10 minutes
schedule.every(10).minutes.do(job)

# Every day at 9:30 AM
schedule.every().day.at("09:30").do(job)

# Every Monday
schedule.every().monday.do(job)

while True:
    schedule.run_pending()
    time.sleep(1)

JavaScript (Node.js)

const cron = require('node-cron');

// Every 15 minutes
cron.schedule('*/15 * * * *', () => {
  console.log('Running every 15 minutes');
});

// Every day at 3:00 AM
cron.schedule('0 3 * * *', () => {
  console.log('Running daily backup');
});

// Every Monday at 9:00 AM
cron.schedule('0 9 * * 1', () => {
  console.log('Running weekly report');
});

Explanation

Five fields make up a cron expression:

FieldAllowed ValuesDescription
Minute0-59Minute of the hour
Hour0-23Hour of the day
Day of Month1-31Day of the month
Month1-12Month of the year
Day of Week0-7 (0 and 7 = Sunday)Day of the week

Special characters:

  • * — any value
  • , — value list separator
  • - — range of values
  • */n — every n steps

Ranges compose with lists, so 0 9 * * 1-5 means weekdays at 9 AM and 0 9,18 * * * means 9 AM and 6 PM.

Cron runs jobs with a minimal environment: PATH is tiny, your shell profile is never loaded, and the working directory is the user’s home. That’s why a script that works in your terminal fails silently at 3 AM — set PATH and SHELL in the crontab itself, or source a profile inside the job.

The two failure modes that bite hardest in production are overlapping runs and timezone handling. Overlapping runs: if a job takes longer than its interval, cron starts another copy anyway, so protect long jobs with flock or max_instances (see Variants). Timezones: system cron follows the machine’s local time — a job pinned to 2:30 AM can fire twice or not at all on DST transition days. CRON_TZ, supported by cronie and by most library schedulers, pins a job to a named timezone instead of the host’s.

Common Schedules

ExpressionSchedule
*/5 * * * *Every 5 minutes
0 * * * *Every hour
0 0 * * *Every day at midnight
0 9 * * 1Every Monday at 9 AM
0 0 1 * *First day of every month
0 0 * * 0Every Sunday at midnight

Most cron implementations also accept named shortcuts: @reboot (once at startup), @hourly, @daily (same as @midnight), @weekly, @monthly, and @yearly.

Variants

Crontab is the easiest place to start, and often the wrong place to stay. The examples below cover the common alternatives: in-app schedulers for tasks that live inside your code, Kubernetes CronJobs for containerized workloads, and systemd timers when you want missed-run catch-up and journal integration on modern Linux. Runnable versions of every example live in the companion repository.

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();

        // Every day at 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("Running backup: " + java.time.LocalDateTime.now());
        }
    }
}

Unlike crontab, Quartz expressions have six fields — seconds come first — and ? stands for “no specific value” in the day fields.

Kubernetes CronJob

apiVersion: batch/v1
kind: CronJob
metadata:
  name: database-backup
spec:
  schedule: "0 3 * * *"  # Every day at 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  # Kill after 1 hour
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 5
  concurrencyPolicy: Forbid  # Prevent overlapping runs

concurrencyPolicy: Forbid prevents overlap with no extra code, and activeDeadlineSeconds caps runaway jobs. If the cluster is down at the scheduled time, startingDeadlineSeconds controls whether the missed run still executes.

Go (robfig/cron)

package main

import (
    "fmt"
    "github.com/robfig/cron/v3"
)

func main() {
    c := cron.New()

    // Every 15 minutes
    c.AddFunc("*/15 * * * *", func() {
        fmt.Println("Health check running...")
    })

    // Every day at 3:00 AM
    c.AddFunc("0 3 * * *", func() {
        fmt.Println("Daily backup running...")
    })

    // With seconds field (optional)
    // c.AddFunc("0 */10 * * * *", func() { ... }) // Every 10 seconds

    c.Start()
    select {} // Block forever
}

systemd Timer (Alternative to 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=Run backup daily at 3:00 AM

[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true  # Run missed jobs after boot

[Install]
WantedBy=timers.target
# Enable and start the timer
$ sudo systemctl enable backup.timer
$ sudo systemctl start backup.timer
$ systemctl list-timers --all

Two advantages over cron: Persistent=true catches up runs missed while the machine was off, and output lands in journalctl -u backup.service instead of disappearing.

Preventing Overlapping Executions with flock

# crontab entry with flock to prevent overlap
0 3 * * * /usr/bin/flock -n /tmp/backup.lock /usr/local/bin/backup.sh

# -n: non-blocking, fail if lock is held
# -w 60: wait up to 60 seconds for the lock
0 * * * * /usr/bin/flock -w 60 /tmp/cleanup.lock /usr/local/bin/cleanup.sh

Cron Environment Variables

# Cron has a minimal PATH. Set it explicitly in crontab:
PATH=/usr/local/bin:/usr/bin:/bin
SHELL=/bin/bash
MAILTO=alerts@example.com

# Or source a profile at the start of each job
0 3 * * * . /home/user/.bashrc && /usr/local/bin/backup.sh

Python with APScheduler (Advanced)

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("Starting database backup...")
    # Backup logic here
    logger.info("Backup completed.")

def health_check():
    logger.info("Health check OK")

scheduler = BackgroundScheduler(timezone="UTC")

# Every day at 3:00 AM UTC
scheduler.add_job(
    backup_job,
    CronTrigger(hour=3, minute=0),
    id="backup",
    max_instances=1,  # Prevent overlap
    coalesce=True,    # Merge missed runs
)

# Every 15 minutes with jitter
scheduler.add_job(
    health_check,
    CronTrigger(minute="*/15"),
    id="health_check",
    max_instances=1,
    jitter=60,  # Random delay up to 60 seconds
)

scheduler.start()

max_instances=1 plays the same role as flock in crontab, and coalesce=True collapses missed runs into one — useful when the scheduler restarts after a deploy.

What Works

  • Use absolute paths for commands and scripts; cron’s minimal PATH won’t find your tools.
  • Redirect output to a log file or /dev/null — otherwise cron mails it to the local user, where nobody reads it.
  • Set CRON_TZ explicitly when a job must fire during business hours; host timezone changes and DST can shift it silently.
  • Keep in-app schedulers alive with a process manager (systemd, PM2) — unlike cron, nothing restarts them if the app dies.
  • Add error handling and alerting — a failing cron job is silent by default.
  • Validate expressions on crontab.guru before pushing them to production.

Common Mistakes

  • Forgetting chmod +x on the script — cron logs Permission denied to syslog and the job exits immediately.
  • Testing scripts with relative paths — they pass in your shell and break under cron’s stripped-down environment.
  • Not handling overlapping executions — a slow job on a short interval spawns competing copies; use flock or max_instances.
  • Ignoring daylight saving time — a job pinned between 2:00 and 3:00 AM local time can fire twice or be skipped entirely on transition days.
  • Pointing * * * * * at a rate-limited API — cron has no backoff, so the throttling has to live inside the job itself.

Performance Tips

  1. Stagger job schedules. Avoid running several heavy jobs at the same time:
# Bad: all at midnight
0 0 * * * /usr/local/bin/backup.sh
0 0 * * * /usr/local/bin/cleanup.sh
0 0 * * * /usr/local/bin/report.sh

# Good: stagger by 30 minutes
0 0 * * * /usr/local/bin/backup.sh
30 0 * * * /usr/local/bin/cleanup.sh
0 1 * * * /usr/local/bin/report.sh
  1. Use jitter for distributed jobs. Add random delay to prevent 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. Set timeouts. Prevent runaway jobs from consuming resources:
# Kill job after 1 hour
0 3 * * * timeout 3600 /usr/local/bin/backup.sh
# Kubernetes CronJob
spec:
  jobTemplate:
    spec:
      activeDeadlineSeconds: 3600
  1. Clean up old job artifacts. Set retention policies:
# Delete backups older than 30 days
0 4 * * * find /backup -name "*.sql" -mtime +30 -delete
# Kubernetes CronJob
spec:
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 5

See Also

  • crontab(5) man page — the authoritative reference for file format and environment variables.
  • crontab.guru — paste an expression, get the next fire times in plain English.
  • Kubernetes CronJob — official docs covering schedule, concurrencyPolicy, and history limits.
  • node-cron — Node.js scheduler (v3+) with an optional seconds field and per-job timezones.
  • APScheduler — Python scheduler (3.x) with cron triggers, jitter, and misfire handling.
  • robfig/cron — Go scheduler; v3 accepts standard expressions and an optional seconds field.
  • Quartz Scheduler — Java job scheduler with six-field cron expressions and persistent job stores.
  • systemd.timer(5) — timer unit reference: OnCalendar syntax, Persistent, RandomizedDelaySec.

Frequently Asked Questions

How do I list the jobs cron will run?

crontab -l shows the current user's entries; sudo cat /var/spool/cron/crontabs/* shows every user's.

Do cron jobs work inside Docker containers?

They do, as long as the container keeps running — a stopped container stops cron too. Many teams schedule on the host instead, or move to Kubernetes CronJobs.

What if a job overruns its own interval?

Cron starts the next copy anyway — there's no built-in guard. flock or a job-level max_instances setting prevents the pile-up.

Can I run something at boot instead of on a schedule?

Yes — @reboot fires once at startup. Cron also accepts @hourly, @daily, @weekly, @monthly, and @yearly shortcuts.

Can cron run jobs more often than once a minute?

Not natively — one minute is the finest unit it understands. systemd timers (OnCalendar supports seconds), node-cron's six-field mode, or an in-app scheduler go below that.

My script works in the terminal but fails under cron — why?

Almost always the environment. Cron uses a minimal PATH and never loads your shell profile; use absolute paths, set PATH and SHELL in the crontab, and check journalctl or /var/log/syslog for the real error.