Cron Job Syntax: How to Schedule Tasks on Linux
You have a backup script. It works perfectly when you run it manually. Now you need it to run every night at 3 AM. You write a crontab entry, deploy it, and check the next morning — nothing happened. The script path is correct. The permissions are fine. But you are hitting the most common cron pitfall: cron runs in a stripped-down shell environment with no PATH, no environment variables from your shell profile, and no current directory assumptions.
Cron has been part of Unix since System V (AT&T Bell Labs, 1983). According to Wikipedia's Unix history documentation, the modern cron format was solidified by Paul Vixie's 1988 rewrite (Vixie cron), which introduced per-user crontabs and the five-field time specification still in use today. Over 40 years later, the syntax is unchanged — and its quirks are just as sharp.
This guide is a complete cron reference: every field, every operator, every special string, environment configuration, logging, and a comparison with systemd timers for modern Linux workloads.
Key Takeaways
- • Five fields, left to right: minute, hour, day-of-month, month, day-of-week. Then the command. Asterisk = every value.
- • Cron has no PATH. Always use absolute paths in cron commands —
/usr/bin/python3notpython3. - • Redirect stderr to debug failures: append
>> /var/log/cron.log 2>&1to every job while troubleshooting. - • Day-of-month AND day-of-week are OR, not AND. If you set both, the job runs when either condition matches.
- • For modern Linux, consider systemd timers. Better logging (journald), dependency management, and catch-up logic for missed runs.
The Crontab Format: All Five Fields
# ┌─────────── minute (0–59)
# │ ┌───────── hour (0–23)
# │ │ ┌─────── day of month (1–31)
# │ │ │ ┌───── month (1–12 or JAN–DEC)
# │ │ │ │ ┌─── day of week (0–7, 0 and 7 = Sunday, or SUN–SAT)
# │ │ │ │ │
# * * * * * command to execute
# Examples
0 2 * * * /usr/bin/backup.sh # Every day at 2:00 AM
30 14 * * 1 /opt/report.py # Every Monday at 2:30 PM
0 0 1 * * /usr/bin/monthly-task # 1st of every month at midnight
*/5 * * * * /usr/bin/health-check # Every 5 minutes
0 9-17 * * 1-5 /usr/bin/business-task # Every hour 9 AM–5 PM, Mon–Fri
0 0 * * 0,6 /usr/bin/weekend-job # Midnight Saturday and Sunday| Field | Allowed values | Special values |
|---|---|---|
| Minute | 0–59 | — |
| Hour | 0–23 | 24-hour clock only |
| Day of month | 1–31 | Vixie cron: L not supported (see note) |
| Month | 1–12 or JAN–DEC | 3-letter abbreviations work on most systems |
| Day of week | 0–7 or SUN–SAT | Both 0 and 7 = Sunday |
Cron Operators: *, ,, -, /
# * (asterisk) — every value in the field
* * * * * cmd # Every minute
# , (comma) — list of values
0 9,12,15 * * * cmd # 9 AM, 12 PM, and 3 PM daily
0 0 * * 1,3,5 cmd # Monday, Wednesday, Friday at midnight
# - (hyphen) — range of values
0 9-17 * * * cmd # Every hour from 9 AM to 5 PM (inclusive)
0 0 * * 1-5 cmd # Monday through Friday at midnight
# / (slash) — step values
*/15 * * * * cmd # Every 15 minutes (0, 15, 30, 45)
0 */2 * * * cmd # Every 2 hours (0, 2, 4, 6, ..., 22)
0 8-18/2 * * * cmd # Every 2 hours from 8 AM to 6 PM
*/10 8-17 * * 1-5 cmd # Every 10 minutes, 8 AM–5 PM, Mon–Fri
# Combining operators
30 6,18 1,15 * * cmd # 6:30 AM and 6:30 PM on the 1st and 15th of each month
0 0 * JAN,JUL * cmd # Midnight in January and July
# ─────────────────────────────────────────────────────────────
# CRITICAL: Day-of-month OR day-of-week (not AND)
# When BOTH dom and dow are set (not *), cron fires when EITHER matches
0 0 1 * 1 cmd
# Runs: 1st of every month midnight OR every Monday midnight
# NOT: Only on Mondays that fall on the 1stSpecial Strings: @reboot, @daily, @hourly
Vixie cron introduced @ shorthand strings as human-readable alternatives to five-field expressions. These are supported by Vixie cron, cronie (Red Hat/Fedora), and dcron (Arch/Alpine) but not by all cron implementations.
| String | Equivalent | When it runs |
|---|---|---|
| @reboot | — | Once at system startup (when crond starts) |
| @yearly / @annually | 0 0 1 1 * | January 1st at midnight |
| @monthly | 0 0 1 * * | 1st of each month at midnight |
| @weekly | 0 0 * * 0 | Sunday midnight |
| @daily / @midnight | 0 0 * * * | Every day at midnight |
| @hourly | 0 * * * * | Start of every hour |
# Usage examples
@reboot /usr/local/bin/start-service.sh
@daily /usr/bin/logrotate /etc/logrotate.conf
@weekly /usr/bin/certbot renew
@monthly /opt/scripts/monthly-report.sh
@hourly /usr/bin/python3 /opt/scripts/metrics.py30+ Real Cron Schedule Examples
# ── Every N minutes ──────────────────────────────────────────
* * * * * cmd # Every minute
*/2 * * * * cmd # Every 2 minutes
*/5 * * * * cmd # Every 5 minutes
*/10 * * * * cmd # Every 10 minutes
*/15 * * * * cmd # Every 15 minutes
*/30 * * * * cmd # Every 30 minutes
# ── Every N hours ─────────────────────────────────────────────
0 * * * * cmd # Every hour (on the hour)
0 */2 * * * cmd # Every 2 hours
0 */4 * * * cmd # Every 4 hours (0, 4, 8, 12, 16, 20)
0 */6 * * * cmd # Every 6 hours
0 */12 * * * cmd # Every 12 hours (midnight, noon)
# ── Specific times ────────────────────────────────────────────
0 0 * * * cmd # Every day at midnight
30 3 * * * cmd # Every day at 3:30 AM
0 9 * * * cmd # Every day at 9:00 AM
0 9,17 * * * cmd # Twice daily: 9 AM and 5 PM
0 8-18 * * * cmd # Every hour from 8 AM to 6 PM
# ── Weekday / weekend ─────────────────────────────────────────
0 9 * * 1-5 cmd # Weekdays at 9 AM (Mon–Fri)
0 0 * * 6,0 cmd # Weekends at midnight (Sat and Sun)
0 9 * * 1 cmd # Every Monday at 9 AM
0 0 * * 5 cmd # Every Friday at midnight
# ── Monthly ───────────────────────────────────────────────────
0 0 1 * * cmd # 1st of every month at midnight
0 0 15 * * cmd # 15th of every month at midnight
0 0 1,15 * * cmd # 1st and 15th at midnight
0 0 L * * cmd # Last day of month (only Quartz/enterprise crons)
# ── Quarterly / annual ────────────────────────────────────────
0 0 1 1,4,7,10 * cmd # First day of each quarter
0 0 1 1 * cmd # January 1st (same as @yearly)
# ── Business hours ────────────────────────────────────────────
*/5 9-17 * * 1-5 cmd # Every 5 min, 9 AM–5 PM, Mon–Fri
0 8 * * 1-5 cmd # 8 AM every weekday
0 17 * * 1-5 cmd # 5 PM every weekday (end-of-day job)
# ── Development / ops common patterns ────────────────────────
0 2 * * * /usr/bin/pg_dump mydb > /backup/db.sql # DB backup 2 AM
0 4 * * 0 /usr/bin/certbot renew --quiet # SSL renewal Sunday 4 AM
*/15 * * * * /usr/bin/python3 /opt/health_check.py # Health check
0 0 1 * * /usr/bin/find /tmp -mtime +7 -delete # Monthly tmp cleanup
@reboot /usr/local/bin/start-worker.sh # On-boot startupManaging Crontabs: Essential Commands
# User crontab commands
crontab -e # Edit your crontab (opens in $EDITOR)
crontab -l # List your current crontab
crontab -r # DANGER: Remove your entire crontab (no confirmation)
crontab -i # Remove with interactive confirmation (safer than -r)
crontab -u alice -e # Edit alice's crontab (root only)
crontab -u alice -l # List alice's crontab (root only)
# Backup and restore
crontab -l > ~/crontab.backup # Back up before editing
crontab ~/crontab.backup # Restore from file
# Check cron daemon status
systemctl status cron # Debian/Ubuntu
systemctl status crond # RHEL/CentOS/Fedora
service cron status # Older systems
# View cron logs
journalctl -u cron # Debian/Ubuntu (systemd)
grep CRON /var/log/syslog # Debian/Ubuntu
grep crond /var/log/cron # RHEL/CentOS
# System-wide crontabs (require root + username field)
# /etc/crontab — main system crontab
# /etc/cron.d/ — drop-in directory (package managers install here)
# /etc/cron.hourly/ — scripts run hourly by run-parts
# /etc/cron.daily/ — scripts run daily
# /etc/cron.weekly/ — scripts run weekly
# /etc/cron.monthly/ — scripts run monthly
# Format for /etc/crontab and /etc/cron.d/ (note the USERNAME field):
# min hour dom month dow USERNAME command
0 2 * * * root /usr/bin/backup.shThe PATH Problem: Why Your Cron Jobs Fail Silently
When you log in to a shell, your .bashrc or .profile sets up PATH, environment variables, and aliases. Cron runs in a minimal, non-interactive, non-login shell that sources none of these files. The default PATH in most cron environments is just /usr/bin:/bin.
# Cron's minimal environment (run "* * * * * env > /tmp/cron-env.txt" to see yours)
HOME=/root
LOGNAME=root
PATH=/usr/bin:/bin # Much less than your shell's PATH
SHELL=/bin/sh # sh, not bash
# ── WRONG: Works in shell, silently fails in cron ──────────────
* * * * * python3 /opt/scripts/task.py # python3 not in /usr/bin:/bin?
* * * * * node /opt/app/worker.js # node not found
* * * * * npm run build # npm not found, no project CWD
# ── RIGHT: Use absolute paths always ────────────────────────────
* * * * * /usr/bin/python3 /opt/scripts/task.py
* * * * * /usr/local/bin/node /opt/app/worker.js
* * * * * cd /opt/app && /usr/local/bin/npm run build
# Find the full path of any binary:
which python3 # → /usr/bin/python3
which node # → /usr/local/bin/node
whereis postgres # Shows multiple paths
# Option: Set PATH at the top of your crontab
# (Applied to all jobs in that crontab)
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
[email protected]
0 2 * * * backup.sh # Now resolves from expanded PATH
# Option: Source profile inside the command
0 2 * * * /bin/bash -l -c '/opt/scripts/backup.sh'
# -l = login shell (sources .profile)
# -c = run commandCron Environment Variables
# Environment variable assignments in crontab (must be at the top)
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
[email protected] # Email output to this address (requires sendmail/MTA)
HOME=/home/alice # Override HOME for this crontab
# Per-job overrides (inline env var)
0 2 * * * DB_HOST=prod-db.internal /opt/backup.sh
# Passing multiple environment variables to a script
0 2 * * * env DB_HOST=prod-db DB_PORT=5432 /opt/backup.sh
# Disable email output entirely (suppresses stdout+stderr)
0 2 * * * /opt/silent-task.sh > /dev/null 2>&1
# Only use this when you have other logging in place — silent failures are dangerous
# Better: Redirect to a log file
0 2 * * * /opt/backup.sh >> /var/log/backup.log 2>&1
# >> appends, > overwrites. 2>&1 merges stderr into stdout
# Conditional logging: only log if there's output (no empty lines on success)
0 2 * * * /opt/check.sh | grep -v "^$" >> /var/log/check.log 2>&1MAILTO is the underused cron feature most developers should know. When set to your email address, cron emails any stdout output from your jobs. This is cheap logging for high-importance tasks. When MAILTO is empty (MAILTO=""), all output is discarded. When unset, cron tries to mail the local system user — which goes nowhere on most VPS setups.
Preventing Overlapping Runs with flock
A cron job that takes longer than its scheduled interval will overlap with the next run. For tasks like database backups, report generation, or data processing pipelines, concurrent runs can corrupt data or waste resources. The standard solution is flock.
# flock: advisory lock on a file — only one instance runs at a time
# -n = non-blocking: skip if lock is held (don't wait)
# -e 1 = exit code 1 if lock cannot be acquired
*/5 * * * * /usr/bin/flock -n /tmp/report.lock /opt/scripts/report.sh
# With logging so you know when skips happen
*/5 * * * * /usr/bin/flock -n /tmp/report.lock /opt/scripts/report.sh >> /var/log/report.log 2>&1 || echo "$(date): report.sh skipped — previous run still active" >> /var/log/report.log
# Inside your script: check if another instance is running
#!/bin/bash
LOCKFILE=/var/run/myapp.lock
exec 9>"$LOCKFILE"
if ! flock -n 9; then
echo "Another instance is running. Exiting."
exit 1
fi
# Lock is held for the rest of the script
# Released automatically when script exits
# Alternative: use a PID file
if [ -f /tmp/task.pid ] && kill -0 "$(cat /tmp/task.pid)" 2>/dev/null; then
echo "Task already running (PID $(cat /tmp/task.pid))"
exit 1
fi
echo $$ > /tmp/task.pid
trap 'rm -f /tmp/task.pid' EXITCron in Docker and Containers
Cron inside Docker containers has a specific problem: the container's environment variables (set via ENV in Dockerfile or --env flags) are not available to cron processes, because cron spawns its own environment.
# Dockerfile: run cron in a container
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y cron
# The standard cron workaround: dump env variables at startup,
# then load them in each cron job
COPY entrypoint.sh /entrypoint.sh
COPY mycron /etc/cron.d/mycron
RUN chmod 0644 /etc/cron.d/mycron && crontab /etc/cron.d/mycron
CMD ["cron", "-f"] # -f = foreground (required for Docker PID 1)
# entrypoint.sh
#!/bin/bash
# Export env vars so cron jobs can access them
printenv | grep -v "no_proxy" >> /etc/environment
exec "$@"
# mycron — uses /etc/environment to get env vars
* * * * * root . /etc/environment; /opt/scripts/task.sh >> /var/log/cron.log 2>&1
# Better alternative for containers: use a dedicated job scheduler
# - Ofelia (Docker-native, labels-based): github.com/mcuadros/ofelia
# - Supercronic: github.com/aptible/supercronic (12-factor, env-aware, no syslog)
# - Kubernetes CronJob resource (if running on k8s)systemd Timers: The Modern Alternative
All major Linux distributions have shipped systemd as PID 1 since 2014–2015 (Ubuntu 15.04, RHEL 7, Debian 8). Systemd timers offer significant advantages over cron for system services: automatic journald logging, dependency management, resource limits, and catch-up logic for missed runs when the system was down.
# Create two files: a .service unit and a .timer unit
# /etc/systemd/system/backup.service
[Unit]
Description=Database Backup
After=network.target
[Service]
Type=oneshot
User=backup
ExecStart=/usr/bin/python3 /opt/scripts/backup.py
StandardOutput=journal
StandardError=journal
# Resource limits (not possible with cron)
MemoryMax=512M
CPUQuota=50%
# /etc/systemd/system/backup.timer
[Unit]
Description=Run backup daily at 2 AM
[Timer]
# Calendar event (cron-style)
OnCalendar=*-*-* 02:00:00
# If system was off at 2 AM, run as soon as it comes back up
Persistent=true
# Random delay up to 5 minutes (spreads load across fleet)
RandomizedDelaySec=5min
[Install]
WantedBy=timers.target
# Activation
systemctl daemon-reload
systemctl enable --now backup.timer
# Management commands
systemctl list-timers # Show all timers and next trigger time
systemctl status backup.timer # Check timer status
journalctl -u backup.service # View all logs for this service
systemctl start backup.service # Run the service immediately (test)
# OnCalendar syntax examples
OnCalendar=hourly # Every hour
OnCalendar=daily # Daily at midnight
OnCalendar=weekly # Weekly on Monday
OnCalendar=Mon *-*-* 09:00:00 # Mondays at 9 AM
OnCalendar=*-*-* *:00/15:00 # Every 15 minutes
OnCalendar=*-01,04,07,10-01 00:00 # First day of each quarter| Feature | Cron | systemd timer |
|---|---|---|
| Configuration simplicity | One line per job | Two files per job |
| Logging | Manual (redirect to file) | Automatic (journald) |
| Missed run recovery | Not built-in | Persistent=true catches up |
| Resource limits | Not supported | MemoryMax, CPUQuota, etc. |
| Dependency management | Not supported | After=, Requires= directives |
| Overlap prevention | Manual (flock) | Type=oneshot prevents overlap |
| Portability | Every Unix-like system | Linux only (systemd) |
Debugging Cron Jobs: A Systematic Checklist
# Step 1: Verify cron is running
systemctl status cron # Debian/Ubuntu
systemctl status crond # RHEL/Fedora
ps aux | grep cron
# Step 2: Check cron logs
journalctl -u cron -f # Follow live
grep CRON /var/log/syslog # Debian/Ubuntu
grep crond /var/log/cron # RHEL/CentOS
# Step 3: Test your script runs manually as the cron user
sudo -u cronuser /bin/sh -c '/path/to/script.sh'
# or simulate cron's minimal environment:
sudo -u cronuser env -i HOME=/home/cronuser PATH=/usr/bin:/bin SHELL=/bin/sh /bin/sh /path/to/script.sh
# Step 4: Check file permissions
ls -la /path/to/script.sh # Should be executable: -rwxr-xr-x
# Fix: chmod +x /path/to/script.sh
# Also check parent directories are accessible to cron user
# Step 5: Check syntax with crontab validator
# Paste your expression at https://crontab.guru
# Or: echo "*/5 * * * * /usr/bin/test" | crontab - (loads into crontab directly)
# Step 6: Add logging to the job
*/5 * * * * /path/to/script.sh >> /var/log/myjob.log 2>&1
# Then watch: tail -f /var/log/myjob.log
# Step 7: Test with a 1-minute job first
* * * * * echo "cron is working $(date)" >> /tmp/cron-test.log
# Then check: cat /tmp/cron-test.log after a minute
# Step 8: Check mail (cron sends output to local mail if MAILTO is not set)
mail # Check local mailbox
# Or check: ls -la /var/mail/$(whoami)For scripts that involve SSH keys, database connections, or other secrets, remember that cron runs without your shell profile. Environment variables must be explicitly provided either at the top of the crontab file or within a wrapper script that loads them from a secure location. See the Environment Variables Best Practices guide for secrets management strategies that work well with cron.
Cron in Application Frameworks
Most application frameworks provide higher-level abstractions over cron for scheduled tasks. These typically handle the PATH problem, logging, error notifications, and distributed locking automatically.
# Node.js: node-cron
import cron from 'node-cron'
// Runs in-process — no separate cron daemon needed
cron.schedule('0 2 * * *', async () => {
try {
await runDatabaseBackup()
console.log('Backup completed:', new Date().toISOString())
} catch (err) {
console.error('Backup failed:', err)
await notifySlack(err.message)
}
}, {
timezone: 'America/New_York', // Timezone support built-in
})
// Python: APScheduler
from apscheduler.schedulers.blocking import BlockingScheduler
from apscheduler.triggers.cron import CronTrigger
scheduler = BlockingScheduler()
@scheduler.scheduled_job(CronTrigger(hour=2, minute=0))
def backup_job():
run_backup()
scheduler.start()
# Python: Celery Beat (distributed, with Redis/RabbitMQ)
from celery import Celery
from celery.schedules import crontab
app = Celery('tasks', broker='redis://localhost:6379/0')
app.conf.beat_schedule = {
'daily-backup': {
'task': 'tasks.backup',
'schedule': crontab(hour=2, minute=0),
},
'health-check': {
'task': 'tasks.health_check',
'schedule': 300.0, # every 5 minutes in seconds
},
}
# Go: robfig/cron
import "github.com/robfig/cron/v3"
c := cron.New(cron.WithSeconds()) // Optional: include seconds field
c.AddFunc("0 0 2 * * *", func() {
runBackup()
})
c.Start()
defer c.Stop()Frequently Asked Questions
What is a cron job?
A cron job is a scheduled task the cron daemon runs automatically at a specified time or interval on Unix-like systems. The name comes from Chronos. Cron has been part of Unix since System V (1983); Paul Vixie's 1988 rewrite established the five-field format still used today. Tasks are defined in a crontab file.
What do the five fields in a crontab entry mean?
Left to right: minute (0–59), hour (0–23), day of month (1–31), month (1–12), day of week (0–7, both 0 and 7 are Sunday). Then the command. Asterisk means "every value." Example: "30 2 * * 1" = 2:30 AM every Monday.
How do I edit the crontab?
Run crontab -e to open your crontab in the default editor. crontab -l lists without editing. crontab -r removes your entire crontab with no confirmation — use crontab -i for interactive removal. Back up first: crontab -l > ~/crontab.backup.
What is the difference between crontab and /etc/crontab?
User crontabs (crontab -e) run as that user and use the standard five-field format. /etc/crontab and /etc/cron.d/ are system-wide, run as root, and require a sixth USERNAME field before the command. /etc/cron.daily, /etc/cron.weekly, etc. are drop-in directories — scripts placed there run at those intervals via run-parts.
Why is my cron job not running?
Five causes: (1) script not executable — chmod +x script.sh, (2) missing PATH — use full absolute paths like /usr/bin/python3, (3) environment variables missing — cron doesn't source .bashrc/.profile, (4) cron daemon not running — systemctl status cron, (5) silent failure — redirect stderr: command >> /var/log/cron.log 2>&1.
What is the difference between cron and systemd timers?
Cron is simpler: one line per job. Systemd timers need two files but offer automatic journald logging, dependency management, resource limits (MemoryMax, CPUQuota), catch-up logic for missed runs (Persistent=true), and overlap prevention (Type=oneshot). For system services on modern Linux, systemd timers are preferred.
Tools for Your Automation Stack
Cron jobs often process data and generate JSON output — use BytePane's JSON Formatter to inspect and validate output. Cron scripts dealing with sensitive configuration need proper secrets management: see the Environment Variables Best Practices guide for .env file patterns and secrets manager integration that works in cron's minimal shell environment.
Open JSON FormatterRelated Articles
Linux Commands Cheat Sheet
Terminal commands for file ops, process management, and scripting.
Environment Variables Best Practices
Managing secrets and config in the minimal cron shell environment.
What Is CI/CD?
Automated pipelines as the modern alternative to cron-based deployments.
Linux File Permissions
chmod, chown — fixing the permissions that cause cron jobs to fail.