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

Table of Contents#

  1. Overview
  2. Cron Expression Syntax
  3. Managing Crontabs
  4. Environment Variables
  5. Output Redirection and Logging
  6. Debugging Failed Jobs
  7. Systemd Timers as Alternative
  8. Edge Cases and Pitfalls
  9. Troubleshooting
  10. See Also
  11. Sources

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#