How the Cron Daemon Works: OS Scheduling, Process Priority, and When to Use systemd Timers Instead
Cron wakes up once per minute, runs with a minimal PATH, and executes via /bin/sh β which is why working shell commands often fail in crontabs. Here's how crond works as a daemon, why environment variables must be set explicitly, process priority with nice values, and why systemd timers handle missed jobs better.
By sadiqbd Β· June 14, 2026
Cron doesn't just run commands on a schedule β it's a daemon process with a specific lifecycle, and understanding that lifecycle explains its quirks
Most developers interact with cron through its expression syntax: 0 2 * * * means "run at 2 AM every day." But cron has behaviours that surprise people who haven't thought about what's running it: why a working shell command sometimes fails in cron, why PATH must be set explicitly, why a missed job doesn't run when the system comes back up, and what actually wakes cron up each minute. These are all properties of how cron works as a Unix daemon.
How the cron daemon works
Cron (specifically crond) is a long-running background process (daemon) that starts at boot and runs continuously. Its core loop is simple:
- Read all crontab files β system crontab (
/etc/crontab,/etc/cron.d/) and user crontabs (/var/spool/cron/crontabs/) - Sleep until the next minute boundary β cron wakes up once per minute, at second 0 of each minute
- Check all scheduled jobs β for each job, check whether the current minute, hour, day-of-month, month, and day-of-week match the crontab expression
- Execute matching jobs β fork a child process to run the command; the parent daemon continues
- Email output β if the job produces any stdout/stderr and
MAILTOis configured, send it via the system mail agent - Go back to sleep β repeat every minute
The implication of sleeping until the minute boundary: cron is accurate to the minute, not the second. A job scheduled at 0 2 * * * runs any time between 02:00:00 and 02:00:59. If you need sub-minute precision, cron is the wrong tool (use sleep within a loop, or a timer with sub-minute resolution).
Why cron jobs fail when shell commands work
The environment problem: cron runs jobs in a minimal environment β not your full login shell. The PATH typically contains only /usr/bin:/bin. Your shell has /usr/local/bin, /home/user/.local/bin, and many other directories in PATH because your .bashrc or .profile adds them.
What fails:
- Commands installed via npm, pip, rbenv, pyenv β not in cron's PATH
- Commands in
/usr/local/binβ sometimes not in cron's PATH - Any command relying on shell aliases or functions
The fix β set PATH explicitly in the crontab:
# Top of crontab
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
# Or at the job level
0 2 * * * /usr/local/bin/python3 /home/user/script.py
The shell problem: cron executes via /bin/sh by default, not /bin/bash. Bash-specific syntax (arrays, [[ ]], $BASHPID, process substitution) will fail. Set the shell explicitly:
SHELL=/bin/bash
0 2 * * * source /home/user/.bashrc && /home/user/script.sh
The working directory problem: cron doesn't set a working directory based on the crontab file location. Relative paths in scripts fail silently because the working directory is typically / or the user's home. Always use absolute paths in cron jobs, or cd at the start:
0 2 * * * cd /var/app && python3 process_data.py
OS process scheduling and priority
When cron forks a child to run a job, that child process competes for CPU with all other processes. By default, it runs at the standard scheduling priority.
Nice values and CPU priority:
Unix processes have a "niceness" value from -20 (highest priority) to +19 (lowest priority, "be nice to others"). Default is 0.
# Run a cron job at reduced priority (nice to other processes)
0 2 * * * nice -n 10 /usr/local/bin/heavy-backup-script.sh
# Run at even lower priority for background processing
0 2 * * * nice -n 19 /usr/local/bin/data-processing-job.py
Why this matters for cron jobs: backup scripts, database dumps, and data processing jobs can consume significant CPU. Running them at nice +10 or +19 means they yield CPU to interactive processes and other higher-priority services without affecting their completion (they just run slower when other work is happening).
ionice for disk I/O priority:
# Best-effort I/O class, priority 7 (lowest best-effort)
0 2 * * * ionice -c 2 -n 7 nice -n 10 /usr/local/bin/backup.sh
On systems with heavy disk I/O, running backup jobs at low I/O priority prevents them from starving application database reads.
Cron vs systemd timers
Modern Linux systems (most distributions since ~2015) have systemd, which provides an alternative to cron: systemd timer units.
Cron advantages:
- Simpler syntax for simple schedules
- Familiar to all Unix administrators
- Runs on any Unix-like system (not just Linux)
- No service unit required β just edit the crontab
Systemd timer advantages:
OnCalendar=for time-based triggers with more expressive syntaxOnActiveSec=,OnBootSec=for relative triggers (e.g., 10 minutes after boot)- Persistent timers: if the system was off when a job should have run, a persistent timer runs it once when the system comes back up (cron misses this entirely)
RandomizedDelaySec=for jitter β spreading job start times to avoid thundering herd on distributed systems- Integration with systemd journal for logging (no separate log configuration)
# /etc/systemd/system/backup.timer
[Unit]
Description=Daily backup timer
[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
RandomizedDelaySec=300
[Install]
WantedBy=timers.target
The Persistent=true line is why many system administrators migrate critical cron jobs to systemd timers β a job that must run daily is guaranteed to run even if the server was down at the scheduled time.
Cron output handling and logging
By default, cron mails job output to the user. On servers without a mail setup, this silently discards output β you never know whether jobs succeeded or failed.
Redirect output explicitly:
# Discard all output (only use if you're confident it works)
0 2 * * * /usr/local/bin/script.sh > /dev/null 2>&1
# Log to file
0 2 * * * /usr/local/bin/script.sh >> /var/log/script.log 2>&1
# Log with timestamp
0 2 * * * date >> /var/log/script.log && /usr/local/bin/script.sh >> /var/log/script.log 2>&1
Structured logging: for important jobs, write to a log aggregation system (Elasticsearch, CloudWatch, Datadog) and alert on failures β rather than hoping someone checks log files.
How to use the Cron Explainer on sadiqbd.com
- Paste any cron expression β get a plain English explanation
- Validate expressions β check that your expression matches the schedule you intended
- Build expressions β construct schedules for common patterns (hourly, daily, weekly, first of month)
- Check next run times β see when an expression will next fire
Frequently Asked Questions
Can cron run jobs more frequently than every minute?
No β the minimum interval is one minute. For sub-minute scheduling: run a script every minute that itself loops with sleep internally, or use a purpose-built tool like Celery Beat (Python), Sidekiq Scheduler (Ruby), or a cloud scheduler with sub-minute support.
Why does cron use a fork-exec model instead of running jobs in-process? Isolation. Each job runs in a fully separate process, with its own memory space, file descriptors, and environment. A job that crashes, leaks memory, or opens too many files has no effect on crond or other running jobs. This is correct Unix process design.
Is the Cron Explainer free? Yes β completely free, no sign-up required.
Try the Cron Explainer free at sadiqbd.com β translate any cron expression to plain English and validate your schedule.