Time-based job scheduler in Unix-like operating systems for automating recurring tasks.


1. Overview#

Cron is a daemon (crond or cron) that runs in the background on Unix-like systems and executes scheduled commands at specified times. It reads configuration files called "crontabs" (cron tables) that define when and what to run.

Cron is used for:

  • Automated backups
  • Log rotation and cleanup
  • System maintenance tasks
  • Periodic data processing
  • Monitoring and health checks

The cron daemon checks for scheduled jobs every minute and executes any that match the current time.

2. Cron Expression Syntax#

A cron expression is a string consisting of five fields that each define a specific unit of time:

 ┌───────────── minute (0-59)
 │ ┌───────────── hour (0-23)
 │ │ ┌───────────── day of month (1-31)
 │ │ │ ┌───────────── month (1-12 or JAN-DEC)
 │ │ │ │ ┌───────────── day of week (0-6, 0=Sunday, or SUN-SAT)
 │ │ │ │ │
 * * * * *  command to execute

2.1 Field Values#

FieldAllowed ValuesDescription
Minute0-59Minute within the hour
Hour0-23Hour of the day (24-hour format)
Day of month1-31Day within the month
Month1-12 or JAN-DECMonth of the year
Day of week0-6 or SUN-SATDay of the week (0 and 7 both mean Sunday)

Note: Day of week values vary across implementations. In standard cron, 0 = Sunday, 1 = Monday, through 6 = Saturday. The value 7 is also accepted as Sunday on most systems.

2.2 Special Characters#

CharacterDescriptionExample
*Matches every value in the field* * * * * (every minute)
,List separator for multiple values1,15 * * * * (minute 1 and 15)
-Specifies a range of values0 8-17 * * * (hours 8 through 17)
/Step value (increment)*/5 * * * * (every 5 minutes)

Step values explained: The / character defines increments. */5 in the minute field means "every 5 minutes" (0, 5, 10, 15, ...). You can combine ranges with steps: 0-30/10 means "every 10 minutes during the first half hour" (0, 10, 20, 30).

2.3 Examples#

* * * * *         Every minute
0 * * * *         Every hour (at minute 0)
0 0 * * *         Every day at midnight
0 0 1 * *         First day of every month at midnight
0 0 1 1 *         January 1st at midnight
30 20 * * SAT     Every Saturday at 20:30
30 20 * * 6       Every Saturday at 20:30 (numeric)
*/5 * * * *       Every 5 minutes
*/15 * * * *      Every 15 minutes
0 8-17/1 * * *    Every hour from 08:00 to 17:00
0 9,18 * * 1-5    At 09:00 and 18:00, Monday through Friday
0 0 */2 * *       Every 2 days at midnight
30 4 1,15 * *     At 04:30 on the 1st and 15th of every month

Predefined Schedules#

Some cron implementations support shorthand strings:

StringEquivalentDescription
@rebootN/ARun once at startup
@yearly / @annually0 0 1 1 *Once a year (Jan 1, midnight)
@monthly0 0 1 * *Once a month (1st, midnight)
@weekly0 0 * * 0Once a week (Sunday, midnight)
@daily / @midnight0 0 * * *Once a day (midnight)
@hourly0 * * * *Once an hour (minute 0)

3. Managing Crontabs#

3.1 User Crontabs#

Each user can have their own crontab, managed with the crontab command:

# Edit the current user's crontab (opens in $EDITOR)
crontab -e

# List the current user's crontab
crontab -l

# Remove the current user's crontab (all entries deleted)
crontab -r

# Edit another user's crontab (requires root)
sudo crontab -u username -e

# List another user's crontab
sudo crontab -u username -l

# Load a crontab from a file
crontab mycrontab.txt

User crontab files are stored in /var/spool/cron/ (RHEL/Arch) or /var/spool/cron/crontabs/ (Debian/Ubuntu). Do not edit these files directly; use crontab -e.

User crontab format (no user field):

# m  h  dom mon dow  command
*/5  *  *   *   *    /home/user/scripts/check_disk.sh
0    3  *   *   *    /home/user/scripts/backup.sh

3.2 System Crontabs#

The system crontab /etc/crontab has an additional user field:

# m  h  dom mon dow  user     command
*/5  *  *   *   *    root     /usr/local/bin/check_disk.sh
0    3  *   *   *    backup   /opt/scripts/backup.sh
17   *  *   *   *    root     cd / && run-parts --report /etc/cron.hourly

3.3 Cron Directories#

Most distributions provide drop-in directories for scheduled tasks:

DirectoryScheduleFormat
/etc/cron.d/Custom (crontab format with user field)Crontab entries
/etc/cron.hourly/Every hourExecutable scripts
/etc/cron.daily/Once per dayExecutable scripts
/etc/cron.weekly/Once per weekExecutable scripts
/etc/cron.monthly/Once per monthExecutable scripts

Files in /etc/cron.d/ use the same format as /etc/crontab (with the user field). Files in the other directories are plain executable scripts run by run-parts.

To add a job via /etc/cron.d/:

# /etc/cron.d/my-backup
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

0 2 * * * root /opt/scripts/backup.sh >> /var/log/backup.log 2>&1

Scripts in /etc/cron.daily/ and similar directories must:

  • Be executable (chmod +x)
  • Not have a file extension (.sh breaks run-parts on some distributions)
  • Have no dots in the filename on Debian-based systems

4. Environment Variables#

Cron jobs run with a minimal environment, not the user's full login shell environment. This is the most common source of "works on the command line but not in cron" issues.

Default cron environment:

SHELL=/bin/sh
PATH=/usr/bin:/bin
HOME=/home/<user>
LOGNAME=<user>

Set environment variables at the top of the crontab:

SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=admin@example.com
HOME=/home/myuser

# Jobs below use the above environment
0 3 * * * /opt/scripts/backup.sh

Key variables:

VariablePurpose
SHELLShell used to execute commands (default: /bin/sh)
PATHSearch path for commands (default is very limited)
MAILTOEmail address for job output; empty string disables email
HOMEWorking directory for jobs
CRON_TZTimezone for schedule evaluation (some implementations)

Best practice: always use absolute paths in cron jobs, or set PATH explicitly in the crontab.

5. Output Redirection and Logging#

By default, cron emails any output (stdout and stderr) to the crontab owner. If no MTA is configured, this output is silently lost.

# Redirect stdout and stderr to a log file
0 3 * * * /opt/scripts/backup.sh >> /var/log/backup.log 2>&1

# Redirect only stderr to a log file, discard stdout
0 3 * * * /opt/scripts/backup.sh > /dev/null 2>> /var/log/backup_errors.log

# Discard all output
0 3 * * * /opt/scripts/backup.sh > /dev/null 2>&1

# Send output via email (default behavior, requires MTA)
MAILTO=admin@example.com
0 3 * * * /opt/scripts/backup.sh

# Disable email for all jobs
MAILTO=""

Using logger to send output to syslog:

0 3 * * * /opt/scripts/backup.sh 2>&1 | logger -t backup-cron

6. Debugging Failed Jobs#

When a cron job does not run or produces unexpected results:

  1. Check the cron log:

    # systemd journal
    journalctl -u cron
    journalctl -u cronie
    
    # Traditional syslog
    grep CRON /var/log/syslog
    grep CRON /var/log/cron
  2. Verify the cron daemon is running:

    systemctl status cron
    systemctl status cronie
  3. Test the command manually as the cron user:

    # Simulate cron's minimal environment
    env -i SHELL=/bin/sh PATH=/usr/bin:/bin HOME=/home/user /opt/scripts/backup.sh
  4. Check file permissions:

    # Script must be executable
    chmod +x /opt/scripts/backup.sh
    
    # Crontab file must not be world-writable
    ls -la /var/spool/cron/
  5. Check for syntax errors in the crontab:

    # Validate by listing (syntax errors shown on load)
    crontab -l
  6. Check /etc/cron.allow and /etc/cron.deny: If /etc/cron.allow exists, only listed users can use cron. If it does not exist, /etc/cron.deny blocks listed users.

7. Systemd Timers as Alternative#

Systemd timers are a modern alternative to cron with better logging, dependency management, and resource control.

Create a service unit (/etc/systemd/system/backup.service):

[Unit]
Description=Daily backup job

[Service]
Type=oneshot
ExecStart=/opt/scripts/backup.sh
User=backup

Create a timer unit (/etc/systemd/system/backup.timer):

[Unit]
Description=Run backup daily at 03:00

[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
RandomizedDelaySec=300

[Install]
WantedBy=timers.target

Enable and start the timer:

sudo systemctl daemon-reload
sudo systemctl enable --now backup.timer

# Check timer status
systemctl list-timers
systemctl status backup.timer

# View logs
journalctl -u backup.service

Comparison:

FeatureCronSystemd Timer
LoggingEmail or manual redirectjournalctl (automatic)
DependenciesNoneFull systemd dependency support
Missed runsLostPersistent=true catches up
Resource limitsNoneCPUQuota, MemoryMax, etc.
Randomized delayNoneRandomizedDelaySec
MonitoringManualsystemctl list-timers

8. Edge Cases and Pitfalls#

Daylight Saving Time#

  • Jobs scheduled during the "spring forward" gap (e.g., 02:30 in a timezone that skips from 02:00 to 03:00) may not run at all
  • Jobs during the "fall back" overlap may run twice
  • Use UTC for critical jobs: set CRON_TZ=UTC or use TZ=UTC in the command

Day of Month and Day of Week#

When both day of month and day of week are specified (not *), cron runs the job when either condition is true (OR logic), not when both are true (AND logic):

# Runs on the 1st AND on every Monday (not just Mondays that are the 1st)
0 0 1 * 1 /opt/scripts/job.sh

Percent Signs#

The % character has special meaning in crontabs: it is converted to a newline, and everything after the first % is sent as stdin. Escape with \%:

# WRONG - % breaks the command
0 0 * * * echo "Today is $(date +%F)"

# CORRECT - escape the percent signs
0 0 * * * echo "Today is $(date +\%F)"

Running on Reboot#

@reboot /opt/scripts/startup.sh >> /var/log/startup.log 2>&1

Troubleshooting#

IssueCauseSolution
Job does not runCron daemon not runningsystemctl start cron or systemctl start cronie
Job does not runCommand not foundUse absolute paths; set PATH in crontab
Job does not runPermission deniedchmod +x the script; check /etc/cron.allow
Job produces no outputOutput not redirectedAdd >> /var/log/job.log 2>&1 to the command
Job runs at wrong timeTimezone mismatchSet CRON_TZ or use UTC
Job does not run during DSTSpring forward gapSchedule at a time outside the DST transition window
% in command breaks itUnescaped percent signUse \% instead of % in crontab entries
Scripts in cron.daily skipFilename contains a dotRemove .sh extension on Debian-based systems
Email flood from cronVerbose command outputRedirect output or set MAILTO=""

See Also#

Sources#