100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Bash

Scheduling Tasks with cron

Learn crontab syntax for scheduling recurring jobs, the difference between user and system crontabs, and common pitfalls like PATH and logging.

System Administration BasicsIntermediate9 min readJul 9, 2026
Analogies

Scheduling Tasks with cron

cron is the standard Unix time-based job scheduler, running as a background daemon (crond) that wakes up every minute, checks all registered crontabs, and launches any job whose schedule matches the current minute. It's the backbone of routine system automation on Linux — log rotation, backups, certificate renewal, report generation, cache warming — anything that needs to run on a fixed recurring schedule without a human triggering it. Each user can maintain their own crontab (edited via crontab -e), and the system also supports system-wide job definitions in /etc/crontab and /etc/cron.d/, plus convenience directories /etc/cron.{hourly,daily,weekly,monthly}/ for simple scripts that don't need custom timing.

🏏

Cricket analogy: cron is like the ground curator who checks a fixed schedule every single minute during the season, rolling the pitch (log rotation), watering it (backups), and repainting boundary lines (cert renewal) automatically without a groundsman standing there.

Crontab Syntax

Each line in a crontab has five time-and-date fields followed by the command to run: minute (0-59), hour (0-23), day-of-month (1-31), month (1-12), and day-of-week (0-7, where both 0 and 7 mean Sunday). An asterisk (*) means 'every value'; comma-separated lists (1,15) mean specific values; a hyphen (9-17) means a range; and a slash (*/15) means a step value — every 15 units starting from the field's minimum. All five fields must match the current time for the job to fire; unlike a simple interval timer, cron reasons purely in terms of matching absolute time fields, which is why 'every 15 minutes' is */15 in the minute field but 'every 15 days' has no direct equivalent (day-of-month doesn't divide evenly, so you'd typically use a script-level check instead).

🏏

Cricket analogy: A crontab's five fields (minute, hour, day, month, weekday) are like specifying an exact delivery in an over — a star means 'any ball,' 1,15 means specific balls, 9-17 means an over range, and */15 means every third ball; but 'every 15 days' has no clean field, like scheduling a Test on an arbitrary rest-day cycle.

bash
# crontab -e opens the current user's crontab in $EDITOR

# ┌───────────── minute (0-59)
# │ ┌───────────── hour (0-23)
# │ │ ┌───────────── day of month (1-31)
# │ │ │ ┌───────────── month (1-12)
# │ │ │ │ ┌───────────── day of week (0-7, Sun=0 or 7)
# │ │ │ │ │
# * * * * * command-to-run

# Every day at 2:30 AM
30 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1

# Every 15 minutes
*/15 * * * * /usr/local/bin/healthcheck.sh

# Every weekday (Mon-Fri) at 9:00 AM
0 9 * * 1-5 /usr/local/bin/send-report.sh

# At 00:00 on the 1st of every month
0 0 1 * * /usr/local/bin/monthly-invoice.sh

# Twice a day, at 06:00 and 18:00
0 6,18 * * * /usr/local/bin/sync-inventory.sh

# List the current user's crontab
crontab -l

# Edit a specific user's crontab (requires root)
sudo crontab -u www-data -e

System Crontabs and cron.d

/etc/crontab and files under /etc/cron.d/ are system-level crontabs with one extra field compared to a user crontab: after the five time fields comes the username the job should run as, before the command itself. This distinction matters — packages that install their own cron jobs (like logrotate or certbot) typically drop a file into /etc/cron.d/ rather than editing a user's personal crontab, because it's cleanly managed by the package manager (removed automatically on uninstall) and version-controllable. The /etc/cron.{hourly,daily,weekly,monthly}/ directories are simpler still: any executable script placed there runs on that cadence automatically via a single line in /etc/crontab that invokes run-parts.

🏏

Cricket analogy: System crontabs in /etc/cron.d/ carry an extra field specifying which player takes the delivery, unlike a personal crontab; a package like logrotate drops its own scheduled task file there automatically, cleanly removed if retired, while /etc/cron.hourly scripts run via run-parts like a fixed pre-match warm-up routine.

cron jobs run with a minimal, often surprising environment: no interactive shell startup files are sourced, PATH typically defaults to just /usr/bin:/bin, HOME may or may not be set as expected, and there is no controlling terminal. A script that works perfectly when you run it by hand can fail silently under cron because it relies on a tool only found via a PATH entry from ~/.bashrc, or on environment variables normally set by your shell profile. Always use absolute paths for commands and files inside cron jobs, explicitly set any required environment variables at the top of the crontab or inside the script itself, and redirect output (>> logfile 2>&1) since cron only emails output by default (via MTA, which is often not configured) and otherwise silently discards it.

Modern systemd-based distributions offer systemd timers as an alternative to cron, with advantages like dependency ordering, better logging via journalctl, the ability to catch up on missed runs after a reboot (Persistent=true), and randomized start delays to avoid thundering-herd effects. However, cron remains extremely widely used because its syntax is universally understood, it requires no unit-file boilerplate for simple jobs, and it works identically across virtually every Unix-like system, not just systemd-based Linux.

Special Strings and Debugging

cron supports convenient shorthand strings in place of the five time fields: @reboot (run once at startup), @daily (equivalent to 0 0 * * *), @hourly, @weekly, @monthly, and @yearly. For debugging a job that isn't firing, check grep CRON /var/log/syslog (Debian/Ubuntu) or journalctl -u cron / journalctl -u crond (RHEL family or systemd-based logging) to confirm cron actually attempted to launch it, verify the crontab syntax with crontab -l, and test the exact command cron would run in a minimal environment using env -i /bin/sh -c '<command>' to reproduce PATH-related failures locally.

🏏

Cricket analogy: @reboot is like the ground's opening ceremony that runs once at the start of the season, while @daily is the routine pitch inspection; to debug a missed job, grep CRON in the ground's logbook or check journalctl, review the fixture list with crontab -l, and re-run the drill in a bare kit with env -i to catch missing equipment.

  • Crontab lines have 5 time fields (minute, hour, day-of-month, month, day-of-week) followed by the command; * means 'every value', */N means a step, and a-b means a range.
  • crontab -e edits the current user's crontab; crontab -l lists it; sudo crontab -u <user> -e edits another user's.
  • System crontabs (/etc/crontab, /etc/cron.d/*) include an extra 'username' field between the time fields and the command.
  • cron runs with a minimal environment (sparse PATH, no shell profile sourced) — always use absolute paths and redirect output explicitly.
  • /etc/cron.{hourly,daily,weekly,monthly}/ let you drop in executable scripts for common cadences without writing crontab syntax.
  • systemd timers are a modern alternative offering better logging and missed-run catch-up, but cron remains the most portable, universally understood scheduler.

Practice what you learned

Was this page helpful?

Topics covered

#Bash#LinuxShellScriptingStudyNotes#DevOps#SchedulingTasksWithCron#Scheduling#Tasks#Cron#Crontab#StudyNotes#SkillVeris