How to Use cron on Linux: Complete Scheduling Guide

How to Use cron on Linux: Complete Scheduling Guide

Tested on: Ubuntu 24.04 LTS · Debian 12 · Fedora 40 — Last updated: June 2026

Cron is Linux's built-in task scheduler — a daemon that wakes every minute, checks its configuration, and fires commands whose schedule matches the current time. If you need to run a backup at 2 AM, rotate logs daily, or poll an API every five minutes, cron handles it reliably without any third-party tooling. This guide covers everything from raw syntax to environment pitfalls, logging strategies, and when to reach for systemd timers instead.

Prerequisites

  • A Linux system running Ubuntu 24.04, Debian 12, Fedora 40, or any modern distribution
  • A non-root user with sudo privileges (required for system-wide jobs)
  • Basic terminal familiarity and a text editor (nano or vim)
  • The cron daemon installed and active — most distros ship it by default

Verify cron is installed and running before proceeding:

# Ubuntu/Debian — service is named 'cron'
systemctl status cron

# Fedora/RHEL/Arch — service is named 'crond'
systemctl status crond

If it is not installed:

# Ubuntu/Debian
sudo apt install cron
sudo systemctl enable --now cron

# Fedora/RHEL
sudo dnf install cronie
sudo systemctl enable --now crond

How Cron Works

Cron is a daemon — it starts at boot and runs continuously in the background. Every minute it wakes, reads all crontab files, and executes any job whose time fields match the current minute. It does not accumulate missed runs; if the system is off at the scheduled time, the job simply does not execute. (If you need missed-run handling, look at anacron, covered at the end of this guide.)

Cron reads from three distinct configuration sources:

  • User crontabs: Each user can maintain a personal crontab via crontab -e. Jobs run as that user with that user's environment and permissions. Stored under /var/spool/cron/crontabs/.
  • System crontab: /etc/crontab — a global file that adds a username field so jobs can run as any system user. Managed by root.
  • Drop-in directories: /etc/cron.d/ for per-application crontab fragments (same format as /etc/crontab), and /etc/cron.hourly/, /etc/cron.daily/, /etc/cron.weekly/, /etc/cron.monthly/ for executable scripts run by run-parts.

crontab Basics

Always use the crontab command to edit your schedule — never edit spool files directly. The command validates your syntax before saving and signals the daemon immediately.

# Open your crontab in your default editor
crontab -e

# List all current entries
crontab -l

# Delete your entire crontab — no confirmation prompt
crontab -r

# Edit or list another user's crontab (as root)
sudo crontab -u www-data -e
sudo crontab -u deploy -l

The first time you run crontab -e, you will be prompted to choose an editor. Your selection is saved to ~/.selected_editor. To change it later:

select-editor

Cron Syntax Explained

Every user crontab job entry has this structure — five time fields followed by the command:

# ┌───────────── 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 both = Sunday, or Sun-Sat)
# │ │ │ │ │
# * * * * *  /path/to/command --args

Operators

*        # wildcard — every valid value
5        # exact value — the 5th minute, or 5th hour, etc.
1-5      # inclusive range — 1, 2, 3, 4, 5
1,3,5    # list — runs on each of these values
*/15     # step — every 15 units: 0, 15, 30, 45
0-23/2   # step with range — every 2 hours from 0: 0, 2, 4 … 22

Reading Time Fields Together

0 2 * * *       # 2:00 AM every day
30 8 * * 1-5    # 8:30 AM every weekday (Mon-Fri)
0 */2 * * *     # top of every 2nd hour: 00:00, 02:00, 04:00 …
15 3 1 * *      # 3:15 AM on the 1st of every month
*/15 9-17 * * 1-5  # every 15 min during business hours, weekdays only
0 0 * * 0       # midnight every Sunday

Critical quirk — day-of-month vs. day-of-week: If you specify a non-wildcard value in both the day-of-month and day-of-week fields, cron uses OR logic, not AND. The entry 0 0 1 * 5 fires at midnight on the 1st of every month and at midnight every Friday — not only on Fridays that happen to fall on the 1st. This surprises nearly everyone the first time.

Special Time Strings

These shortcuts replace the five-field block entirely. They are clearer to read and safe to use in any crontab:

StringEquivalentMeaning
@rebootOnce at system startup
@yearly / @annually0 0 1 1 *Jan 1st at midnight
@monthly0 0 1 * *First of every month at midnight
@weekly0 0 * * 0Every Sunday at midnight
@daily / @midnight0 0 * * *Every day at midnight
@hourly0 * * * *Top of every hour
@reboot  /home/deploy/start-agent.sh
@daily   /home/deploy/scripts/cleanup.sh >> /var/log/cleanup.log 2>&1
@hourly  /usr/local/bin/health-check

@reboot is especially useful for launching user-space services or mounting FUSE filesystems without writing a full systemd unit file.

Practical Examples

Nightly Backup at 2 AM

0 2 * * * /home/deploy/scripts/backup.sh >> /var/log/backup.log 2>&1

Disk Space Alert Every Hour

0 * * * * df -h | mail -s "Disk Usage $(hostname) $(date +%F)" ops@example.com

Clean Up Temp Files Weekly

0 0 * * 0 find /tmp -type f -mtime +7 -delete

PostgreSQL Dump — Weekdays at 6 PM

0 18 * * 1-5 /usr/bin/pg_dump mydb > /backup/db_$(date +%Y%m%d).sql

Note the escaped %. The percent sign (%) is special in crontab — everything after an unescaped % is treated as a newline and passed to the command on stdin. Always escape % characters in crontab entries.

Every 15 Minutes During Business Hours

*/15 9-17 * * 1-5 /opt/myapp/bin/sync-queue >> /var/log/sync.log 2>&1

Run at Boot, Then Daily

@reboot   /opt/myapp/bin/worker --daemon
0 4 * * * /opt/myapp/bin/daily-report

System-Wide Cron Jobs

/etc/crontab

The system crontab inserts a username field between the time specification and the command:

cat /etc/crontab
# min  hour  dom  month  dow  user     command
17     *     *    *      *    root     cd / && run-parts --report /etc/cron.hourly
25     6     *    *      *    root     test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.daily )
47     6     *    *      7    root     test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.weekly )
52     6     1    *      *    root     test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.monthly )

/etc/cron.d/ — Per-Application Jobs

This is the correct place for application or package cron jobs. Each file follows the same format as /etc/crontab. Keeping jobs in separate files makes them easy to audit, disable, or remove without touching the system crontab.

sudo nano /etc/cron.d/myapp
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
MAILTO=ops@example.com

# Run as the 'deploy' user every night at 1 AM
0 1 * * * deploy /opt/myapp/scripts/maintenance.sh >> /var/log/myapp-maintenance.log 2>&1

Filename rules for /etc/cron.d/: Files must consist only of alphanumerics, hyphens, and underscores. Names containing dots (e.g., myapp.conf) are silently ignored by run-parts on Debian-based systems. Name your file myapp or myapp-maintenance, not myapp.cron.

Environment Variables in Crontab

This is where most cron debugging time goes. Cron runs jobs with a minimal environment — nothing like your interactive shell session. The PATH is typically only /usr/bin:/bin. Variables like HOME, DISPLAY, NVM_DIR, VIRTUAL_ENV, and any exports from your .bashrc or .profile are not present.

You can set variables at the top of your crontab — they apply to all jobs below them:

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

0 2 * * * /home/deploy/scripts/backup.sh

Setting MAILTO to an email address causes cron to mail the output of every job to that address. Set MAILTO="" to suppress all email. Setting it per-job using redirection gives you the most control.

The safest approach is to always use absolute paths for both commands and any files they reference, and to source your environment explicitly inside scripts that need it:

#!/bin/bash
# At the top of your cron-invoked script:
source /home/deploy/.profile
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"

# Rest of your script...
node /opt/myapp/index.js

Logging Cron Output

By default, cron emails any output to the local system user. In practice, configure explicit logging in each job entry:

# Append stdout and stderr to a log file
0 2 * * * /home/deploy/backup.sh >> /var/log/backup.log 2>&1

# Separate stdout and stderr into different files
0 2 * * * /home/deploy/backup.sh >> /var/log/backup.log 2>>/var/log/backup-errors.log

# Discard all output (silent jobs)
*/5 * * * * /usr/local/bin/check.sh > /dev/null 2>&1

# Add timestamps to log output — useful for multi-run scripts
0 * * * * /usr/local/bin/health.sh 2>&


Go up