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.
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.
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:
| Field | Allowed Values | Description |
|---|---|---|
| Minute | 0-59 | Minute of the hour |
| Hour | 0-23 | Hour of the day |
| Day of Month | 1-31 | Day of the month |
| Month | 1-12 | Month of the year |
| Day of Week | 0-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
| Expression | Schedule |
|---|---|
*/5 * * * * | Every 5 minutes |
0 * * * * | Every hour |
0 0 * * * | Every day at midnight |
0 9 * * 1 | Every Monday at 9 AM |
0 0 1 * * | First day of every month |
0 0 * * 0 | Every 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
PATHwon’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_TZexplicitly 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 +xon the script — cron logsPermission deniedto 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
flockormax_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
- 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
- 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()
- 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
- 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.
Related Resources
APScheduler BackgroundScheduler: Prevent Overlapping Jobs
Run cron-like jobs in Python using APScheduler. Covers interval, cron, and date triggers, job stores, and background scheduling.
RecipeRun Scheduled Jobs with Serverless Functions
How to replace cron jobs with serverless scheduled functions for backups, reporting, cleanup, and periodic maintenance tasks.
RecipeBackground Jobs: Task Queues, Cron & Workers
Offload slow work from HTTP requests: run background jobs with task queues, cron scheduling, and workers in Python, JavaScript, and Java.
RecipeSchedule and Monitor DAGs with Apache Airflow
Define, schedule, and monitor Airflow DAGs with operators, sensors, XCom, task dependencies, and the TaskFlow API.
RecipeCI/CD Pipeline Setup
Set up automated CI/CD pipelines for testing, building, and deploying applications with GitHub Actions and what works.
RecipeRetry Logic with Exponential Backoff
How to implement resilient retry logic with exponential backoff and jitter for transient failures in network and API calls.