Cron has been scheduling Unix jobs since 1975, and in 2026 it remains the lowest-friction way to run a script on a schedule. Cloud functions cost money per invocation, container orchestrators need infrastructure — cron is free and already installed. Knowing it well saves you from overengineering simple automation.
What changed in 2026
- Systemd timers are mainstream on modern Linux — they log to journald and support dependency ordering, so they are preferred over cron on systemd systems for anything beyond a quick one-liner.
- macOS Sequoia kept launchd as the native scheduler; cron works but Apple deprecated it in favour of
launchctl for persistent daemons.
- Cloud cron is everywhere — GitHub Actions scheduled workflows, Cloud Scheduler (GCP), EventBridge Scheduler (AWS), and Fly.io Machines all speak cron syntax in YAML. Understanding the syntax still matters.
- Container cron gotchas are well-documented; running cron inside Docker requires explicit signal handling and PID 1 concerns.
The five-field syntax
# ┌── minute (0-59)
# │ ┌── hour (0-23)
# │ │ ┌── day of month (1-31)
# │ │ │ ┌── month (1-12)
# │ │ │ │ ┌── day of week (0-7, 0 and 7 = Sunday)
# │ │ │ │ │
# * * * * * command
Common examples:
# Every day at 2:30 AM
30 2 * * * /usr/local/bin/backup.sh
# Every 15 minutes
*/15 * * * * /opt/scripts/healthcheck.sh
# Monday–Friday at 9 AM
0 9 * * 1-5 /home/deploy/reports.py
# First day of every month at midnight
0 0 1 * * /usr/local/bin/monthly_cleanup.sh
Use crontab.guru to verify expressions before deploying.
What changed in 2026
Environment — the biggest gotcha
Cron runs with a minimal environment: HOME, USER, and a stripped PATH (/usr/bin:/bin). Nothing else. A command that works in your shell may silently fail in cron.
# BAD — depends on user PATH
backup.sh
# GOOD — absolute path
/usr/local/bin/backup.sh
# GOOD — set PATH at the top of crontab
PATH=/usr/local/bin:/usr/bin:/bin
Always use absolute paths for the command and any files it references.
Editing the crontab
crontab -e # edit your crontab (opens $EDITOR)
crontab -l # list current jobs
crontab -r # remove all jobs (dangerous — no confirm!)
sudo crontab -e -u deploy # edit another user's crontab
System-wide jobs live in /etc/cron.d/, /etc/cron.daily/, etc. — files placed there run as root.
Logging and visibility
Cron mails output to the local user by default — which no one reads. Redirect explicitly:
# Discard all output (use only if you're sure the script is working)
0 3 * * * /opt/bin/cleanup.sh > /dev/null 2>&1
# Log to a file (append)
0 3 * * * /opt/bin/cleanup.sh >> /var/log/cleanup.log 2>&1
# Log with timestamps via systemd-cat (systemd systems)
0 3 * * * systemd-cat -t cleanup /opt/bin/cleanup.sh
The >> logfile 2>&1 pattern is the minimum acceptable logging for any production job.
How to pick the right scheduler
| Need |
Tool |
| Simple repeating script |
cron |
| Log to journald, dependencies |
systemd timer |
| Retry on failure |
systemd timer or job queue |
| Cloud-native, no server |
Cloud Scheduler / EventBridge |
| CI pipeline schedule |
GitHub Actions schedule: |
| Complex DAG of jobs |
Airflow, Prefect, or similar |
| macOS background task |
launchd (launchctl) |
Cron vs systemd timers
| Feature |
cron |
systemd timer |
| Syntax |
five-field |
calendar expressions |
| Logging |
mail / redirect |
journald (automatic) |
| Missed-run catch-up |
no |
yes (Persistent=true) |
| Dependency ordering |
no |
yes (After=, Requires=) |
| Ease of setup |
one line |
two files (.timer + .service) |
| Requires systemd |
no |
yes |
For a new job on a modern Ubuntu/Debian server, prefer a systemd timer. For portability or a quick script, cron wins.
Common mistakes
Forgetting to redirect output. The job fails silently and you find out days later when nothing happened.
Relative paths in scripts called by cron. The working directory is usually $HOME, not where your script lives. Use cd /path/to/project && ./script.sh or absolute paths everywhere.
No lock file for long-running jobs. If a job takes longer than its interval, two instances run in parallel. Guard with flock:
0 * * * * flock -n /tmp/myjob.lock /opt/bin/myjob.sh
Editing /etc/crontab directly. Use /etc/cron.d/ instead; files there are named and easier to manage.
Not testing outside cron first. Run your command manually in a clean shell (env -i HOME=$HOME /usr/local/bin/script.sh) before adding it to crontab.
What to skip
- Cron for retry logic — if failure means re-run, use a job queue (Sidekiq, BullMQ, Celery) or systemd's
OnFailure=.
- Cron inside Docker containers — the complexity of signal handling, PID 1, and log forwarding usually means you're better off with a sidecar or an external scheduler.
@reboot for critical daemons — use systemd/init for proper service management with restart policies.
FAQ
How do I debug a failing cron job?
Redirect stderr to a log file, run the exact command manually with a stripped environment (env -i HOME=$HOME PATH=/usr/bin:/bin the-command), and check /var/log/syslog or journalctl for cron execution records.
Can cron run every second?
No — the minimum interval is one minute. For sub-minute scheduling use a while-loop script, systemd timers with OnUnitActiveSec, or a proper scheduler.
What user does cron run as?
User crontabs run as that user. /etc/cron.d/ jobs specify the user in the crontab line. System directories like cron.daily run as root unless otherwise configured.
Does cron run if the machine is off?
No. Missed jobs are skipped by default. Use anacron for machines that are not always on, or Persistent=true in a systemd timer.
Where to go next