DevOps & Workflow16 min read

    What is crontab and how to use it

    By DanaServer Monitoring & Linux
    Share

    "What is crontab" is one of those questions where the short answer is "the file" and the useful answer is "the format, the command, and the scheduler behind it" all at once. Crontab is, depending on which sentence you're in: a file format describing periodic jobs; a command-line tool for editing those files (crontab -e); the per-user file itself (each user has their own); and, by extension, the umbrella term for everything having to do with cron-style scheduling on Linux. The cron daemon (cron or crond) reads crontabs and runs the jobs they describe. The job lives on every Linux distribution and most Unix-likes since the late 1970s, and it's still where most periodic work on a Linux server is scheduled.

    This guide explains what crontab is from first principles: the syntax, where the files live, how to edit them, the five-field schedule format, the practical examples that cover 90% of real use, the gotchas that catch most people once, and when systemd timers are a better fit than cron in 2026.


    cron, crontab, cron job — the terminology

    The three words sound interchangeable; they aren't:

    • cron is the daemon — cron on Debian/Ubuntu, crond on RHEL/CentOS/Rocky/Alma — that runs in the background and fires scheduled jobs.
    • crontab is the file format and the per-user file itself. Each user can have one crontab (in /var/spool/cron/crontabs/<user> on Debian/Ubuntu, /var/spool/cron/<user> on RHEL family). The cron daemon reads each user's crontab and the system-wide ones and acts on them.
    • A cron job is a single line in a crontab — one schedule plus one command.

    When someone says "add it to the crontab", they mean "add a line to the crontab file". When they say "set up a cron job", they mean the same thing. The daemon (cron / crond) is what actually runs the commands, but you almost never interact with it directly — you edit crontabs, and the daemon picks up your changes.


    The five-field schedule syntax

    Every cron job line has the same shape: a five-field schedule, then the command:

    * * * * *  command
    │ │ │ │ │
    │ │ │ │ └─ day of week    (0–7, where 0 and 7 are Sunday; or names: SUN-SAT)
    │ │ │ └─── month          (1–12; or names: JAN-DEC)
    │ │ └───── day of month   (1–31)
    │ └─────── hour           (0–23)
    └───────── minute         (0–59)
    

    The simplest reading of * * * * *: "every minute, every hour, every day, every month, every day of the week". Each * is replaced with a specific value (or pattern) to narrow the schedule.

    Common patterns

    # Every minute (the most-frequent legal cron interval)
    * * * * *  /usr/local/bin/heartbeat.sh
    
    # Every 5 minutes
    */5 * * * *  /usr/local/bin/poll.sh
    
    # Top of every hour
    0 * * * *  /usr/local/bin/hourly-job.sh
    
    # Every day at 03:00
    0 3 * * *  /usr/local/bin/nightly-backup.sh
    
    # Every Monday at 09:00
    0 9 * * 1  /usr/local/bin/weekly-report.sh
    
    # 1st of every month at 04:00
    0 4 1 * *  /usr/local/bin/monthly-rollup.sh
    
    # Weekdays only, 09:00
    0 9 * * 1-5  /usr/local/bin/business-hours.sh
    
    # Every 15 minutes between 09:00 and 17:00, weekdays
    */15 9-17 * * 1-5  /usr/local/bin/work-hours-poll.sh
    
    # At 02:30 and 14:30 every day
    30 2,14 * * *  /usr/local/bin/twice-daily.sh
    
    # Every 6 hours starting at midnight (00, 06, 12, 18)
    0 */6 * * *  /usr/local/bin/every-six-hours.sh
    

    The four operators that compose these patterns:

    Operator Meaning Example
    * Every value * * * * * (every minute)
    , List 0,15,30,45 * * * * (4 times per hour)
    - Range 0 9-17 * * * (every hour 09:00–17:00)
    / Step */10 * * * * (every 10 minutes)

    Combine them freely: */5 9-17 * * 1-5 is "every 5 minutes during business hours on weekdays".


    Special @ shortcuts

    Most cron implementations also accept named shortcuts, which are easier to read for common cadences:

    Shortcut Equivalent Meaning
    @reboot n/a (special) Run once at system startup
    @yearly / @annually 0 0 1 1 * Once a year, midnight Jan 1
    @monthly 0 0 1 * * Once a month, midnight on day 1
    @weekly 0 0 * * 0 Once a week, midnight Sunday
    @daily / @midnight 0 0 * * * Once a day, midnight
    @hourly 0 * * * * Once an hour, on the hour
    @daily   /usr/local/bin/backup.sh
    @hourly  /usr/local/bin/sync.sh
    @reboot  /usr/local/bin/start-helper.sh
    

    @reboot is the special one — there's no five-field equivalent. It runs the command once when cron itself starts (which on systemd-based distros means "shortly after boot when cron.service is started"). Convenient for "start this on boot" without writing a systemd service, but for anything important, a real systemd service unit is more reliable.


    Where crontabs live

    There are five places cron jobs can be defined. Knowing which is which saves a lot of "I added it but it didn't run":

    1. Per-user crontabs

    Each user can have one crontab:

    crontab -e          # edit your crontab
    crontab -l          # list your crontab
    crontab -r          # remove your crontab (no prompt — be careful)
    sudo crontab -e -u alice    # edit alice's crontab as root
    

    These files live in /var/spool/cron/crontabs/<user> (Debian/Ubuntu) or /var/spool/cron/<user> (RHEL/CentOS/Rocky/Alma). Don't edit them directly — crontab -e does syntax validation and reload signalling that hand-editing skips.

    User crontabs don't have a username column. The file is the user.

    2. /etc/crontab

    The system-wide crontab. Lives at exactly /etc/crontab. Has an extra username column between schedule and command:

    # m   h   dom mon 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 )
    

    Most distros ship a default /etc/crontab that just kicks off the /etc/cron.hourly, /etc/cron.daily, etc. directories — see the next section.

    3. /etc/cron.d/

    Drop-in directory for system cron jobs. Each file is its own crontab, with a username column. Same format as /etc/crontab:

    sudo tee /etc/cron.d/my-app <<'EOF'
    # Run every 5 minutes as the app user
    */5 * * * * appuser /usr/local/bin/my-task.sh
    EOF
    

    This is the right home for cron jobs managed by configuration management (Ansible, Puppet, Chef) — file-based, with a username column, picked up automatically without service reload on most distros.

    4. /etc/cron.{hourly,daily,weekly,monthly}/

    Drop-in directories for executables (not crontab lines — actual scripts):

    sudo tee /etc/cron.daily/cleanup <<'EOF'
    #!/bin/sh
    find /tmp -mtime +7 -delete
    EOF
    sudo chmod +x /etc/cron.daily/cleanup
    

    The exact time depends on /etc/crontab and (on Debian/Ubuntu) anacron, which fills in for jobs that should have run while the host was off. Anacron's default is roughly 06:25 / 06:47 / 06:52 / 06:52 for daily/weekly/monthly/hourly — close enough to "early morning" but not a precise time.

    If precise timing matters, use /etc/cron.d/ instead. If "roughly daily / weekly / monthly" is enough, the drop-in directories are the most maintenance-free way.

    5. Anacron (/etc/anacrontab)

    Anacron handles missed jobs on hosts that aren't always on (laptops, infrequent servers). It runs jobs that should have run but were missed because the host was off, with a small delay after boot. Most desktops and many Debian/Ubuntu installs use anacron under the hood for cron.daily / cron.weekly / cron.monthly.

    You usually don't edit anacron directly — the cron drop-in directories are the user-facing surface. But if cron.daily jobs are firing at unexpected times on a desktop, anacron is the reason.


    Editing crontabs safely

    Always use crontab -e, never edit the spool files directly. The wrapper:

    • Validates syntax before saving (rejects an invalid file with the changes still in your editor for fixing).
    • Signals the cron daemon to reload, so your changes take effect without a service restart.
    • Edits the right file regardless of which distro / user.

    The first run typically asks which editor to use; pick nano (friendly), vim (default on RHEL family, fastest if you know it), or set EDITOR=vi / VISUAL=nano in your shell to force a specific one.

    To save changes from crontab -e to a real file (e.g. for backup or version control):

    crontab -l > my-crontab.txt
    # ... commit my-crontab.txt to your dotfiles / config repo ...
    crontab my-crontab.txt    # install from a file
    

    Backing up crontabs is genuinely useful — crontab -r (remove) has no prompt, no undo, and silently wipes the user's entire crontab.


    The cron environment is hostile

    The single most common "my cron job doesn't work" cause: cron runs jobs in a minimal environment that's nothing like your interactive shell.

    Cron sets:

    • HOME (the user's home directory)
    • LOGNAME, USER (the user)
    • PATH=/usr/bin:/bin (minimal — does not include /usr/local/bin, /sbin, /usr/sbin, or anything you've added in .bashrc)
    • SHELL=/bin/sh (note: not bash, even if your login shell is bash)

    Cron does not:

    • Source ~/.bashrc, ~/.profile, /etc/profile, or any login files.
    • Have access to your aliases or functions.
    • Set most of the environment variables you've come to rely on (LANG, LC_*, EDITOR, etc.).
    • Run interactively (so prompts, password readlines, read-from-stdin all hang or fail).

    Three habits that prevent most cron environment bugs:

    1. Use absolute paths. /usr/local/bin/my-script.sh, not my-script.sh. /usr/bin/curl, not curl.
    2. Set PATH explicitly at the top of the crontab if your scripts need a fuller PATH:
      PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
      SHELL=/bin/bash
      * * * * * /path/to/script.sh
      
    3. Test the job in the cron environment before deploying:
      env -i HOME="$HOME" PATH=/usr/bin:/bin SHELL=/bin/sh /path/to/script.sh
      
      If it works under that, it'll work in cron.

    Capturing output and errors

    By default, cron emails the user any output the command produces (stdout + stderr). On a typical server, that mail goes nowhere — there's no MTA, no inbox — and the output is silently lost. Three ways to do it properly:

    Redirect to a log file

    * * * * * /usr/local/bin/job.sh >> /var/log/job.log 2>&1
    

    >> /var/log/job.log appends stdout; 2>&1 redirects stderr to the same place. Now you have a record. Pair with logrotate so the file doesn't grow forever.

    Discard output entirely

    * * * * * /usr/local/bin/quiet-job.sh > /dev/null 2>&1
    

    Only do this if you're sure the job logs internally — otherwise you're flying blind.

    Send mail to a real destination

    MAILTO=ops@example.com
    * * * * * /usr/local/bin/job.sh
    

    MAILTO set at the top of a crontab routes any output to that address. Requires a working MTA (Postfix, sendmail, etc.) on the host.

    A useful pattern: combine "log everything" with "mail on failure":

    * * * * * /usr/local/bin/job.sh >> /var/log/job.log 2>&1 || echo "job.sh failed at $(date)" | mail -s "cron failure" ops@example.com
    

    Common mistakes

    • % in the command% has special meaning in crontab files (it inserts a newline into stdin). Quote it or escape it: \% or use a wrapper script. Common bite when the command includes date '+%Y-%m-%d'.
    • Forgetting absolute paths. Half the cron debugging done in the world is "why does this work in my shell?" — because the shell's PATH finds the binary and cron's doesn't.
    • Editing /var/spool/cron/... directly. Skips the validation and reload that crontab -e does. Results in changes that don't take effect until next reboot, or syntax errors that silently disable the entire crontab.
    • Both crontab -e and /etc/cron.d/ for the same job. The job runs twice. Use one or the other.
    • Missing trailing newline. Some cron implementations refuse to read a file that doesn't end with a newline. Most modern editors handle this automatically; some don't.
    • Day-of-week ambiguity for Sunday. Cron accepts 0 and 7 for Sunday. 1 is Monday. The traditional "1 = Monday" feels off for people used to Sunday-first calendars. When in doubt, use the names: MON, TUE, etc.
    • Day-of-month and day-of-week behave like OR, not AND. 0 0 1 * 1 means "midnight on the 1st of the month, or on Monday" — not "first Monday of the month". For "first Monday of the month", use 0 0 1-7 * 1 (a Monday in the first week is the same as the first Monday).
    • Timezone confusion. Cron uses the system's timezone. timedatectl confirms what's set; if you're scheduling to a specific local time, that's what cron uses. If you migrate the host to a new timezone, the schedules effectively change — verify after the migration.

    systemd timers: the modern alternative

    On systemd-based distros (Ubuntu 16.04+, Debian 8+, RHEL/CentOS/Rocky/Alma 7+), systemd timers are usually a better fit than cron for non-trivial scheduling. The trade-offs:

    cron / crontab systemd timer
    Setup One line in a file Two files (.service + .timer)
    Logging DIY (redirect to log file) Automatic via journalctl -u <service>
    Missed runs Skipped silently Persistent=true runs them on next start
    Randomised delay DIY RandomizedDelaySec= built in
    Dependency ordering None Full systemd unit dependencies
    Reload Automatic on crontab -e systemctl daemon-reload
    Familiar to most operators Yes Less so

    A simple systemd timer equivalent of 0 3 * * * /path/to/backup.sh:

    /etc/systemd/system/backup.service:

    [Unit]
    Description=Nightly backup
    
    [Service]
    Type=oneshot
    ExecStart=/path/to/backup.sh
    

    /etc/systemd/system/backup.timer:

    [Unit]
    Description=Run backup daily
    
    [Timer]
    OnCalendar=*-*-* 03:00:00
    Persistent=true
    
    [Install]
    WantedBy=timers.target
    
    sudo systemctl daemon-reload
    sudo systemctl enable --now backup.timer
    sudo systemctl list-timers
    

    For new system-level scheduling on a modern distro, systemd timers are the durable choice. For per-user jobs, quick one-liners, or anything where an operator unfamiliar with systemd will need to maintain it, cron is still fine.

    For the specific case of a scheduled reboot — where systemd's Persistent=true and RandomizedDelaySec are big wins — see the deeper walkthrough in How to set a Linux cron job to reboot a server.


    Monitoring cron jobs (catching the silent failures)

    Cron's worst failure mode isn't "the job errored" — it's "the job didn't run". A typo in the schedule, a corrupted crontab, a service that didn't start after reboot, or simply a host that was off when the schedule fired all silently produce no output, no error, and no notification.

    The right safety net is heartbeat-style monitoring: every cron job pings a monitoring service when it runs. The monitor knows the expected cadence; if a heartbeat is missing, it pages.

    0 3 * * * /usr/local/bin/backup.sh && curl -fsS https://heartbeat.example.com/abc123 > /dev/null
    

    The && ensures the heartbeat only fires if backup.sh succeeded. If the script fails or doesn't run, the heartbeat is missing, and you find out within minutes — not the next morning when someone notices the backup didn't happen.

    Xitoring's cron job monitoring provides per-job heartbeat URLs with expected-cadence configuration and alerting on missed pings or late completions. Pair with server monitoring so you can correlate "the cron job didn't run" against "the host was offline at the time" without two separate dashboards.

    The KB on heartbeat uptime monitoring covers the setup in detail; the broader operational picture for cron-driven workflows links back to reboot scheduling and the underlying process inspection used to confirm jobs are running.


    Operational tips

    • Version-control your crontabs. crontab -l > crontab.txt once, commit, and now changes are reviewable. crontab crontab.txt reinstalls. Better yet, use /etc/cron.d/ files managed by Ansible / Puppet / Chef.
    • Don't put crontab -r near your crontab -l muscle memory. They're one keystroke apart and -r has no prompt. Aliasing alias cronr='crontab -r -i' (the -i adds a confirmation, on some distros) is mild defensive coding.
    • Schedule heavy jobs off-peak. Database backups at 03:00, log rotations at 04:00, report generation at 05:00 — but don't pile them all on the same minute. Spread across 02:30, 03:15, 04:00 etc. so they don't compete for I/O.
    • Use flock for jobs that mustn't overlap. If a job runs every 5 minutes but might take 10, the second invocation can stomp on the first. flock -n /var/run/job.lock /usr/local/bin/job.sh skips the second run if the first hasn't finished.
    • Add set -euo pipefail to cron-fired bash scripts. A failed command early in the script then silently continuing is the worst kind of cron bug. Strict mode catches it.
    • Log start AND end. Logging only at the end means you can't tell "didn't run" from "is still running". Log both, with timestamps.
    • Use a wrapper script for non-trivial jobs. A one-liner with five pipes inside a crontab is impossible to debug. Move the logic to /usr/local/bin/<job>.sh, leave the crontab line as just 0 3 * * * /usr/local/bin/<job>.sh >> /var/log/<job>.log 2>&1.

    Summary

    What is crontab — to summarise:

    1. Crontab is the file format and per-user file that describes scheduled jobs. Cron is the daemon that runs them. A cron job is one line of a crontab.
    2. Five-field syntax: minute hour day-of-month month day-of-week command. * is "every"; , is list; - is range; / is step. Combine freely.
    3. @reboot, @hourly, @daily, @weekly, @monthly, @yearly are named shortcuts for common cadences.
    4. Five places crontabs live: user crontabs (crontab -e), /etc/crontab, /etc/cron.d/, /etc/cron.{hourly,daily,weekly,monthly}/, and anacron. Pick by responsibility (per-user vs system) and config-management style (file vs spool).
    5. The cron environment is minimal. PATH=/usr/bin:/bin, no rc files. Use absolute paths; set PATH at the top of the crontab if needed; test with env -i HOME=$HOME PATH=/usr/bin:/bin.
    6. Capture output explicitly. >> /var/log/job.log 2>&1 or MAILTO=. Don't let cron's default mail-to-local-user silently swallow it.
    7. Day-of-month and day-of-week are OR, not AND. 0 0 1 * 1 is "1st of month or Monday".
    8. systemd timers are the modern alternative for system-level scheduling — better logging, missed-run handling, randomised delay. For per-user or simple jobs, cron is still fine.
    9. Monitor the jobs that matter. Heartbeat URLs are the durable answer: every job pings on success, the monitor pages on a missed ping. Catches the silent failures cron itself never reports.

    Crontab is one of those tools where the syntax is small enough to fit on an index card, but the gotchas are the difference between "this just works" and "this has been silently broken for three months". The 30 minutes spent learning the gotchas — environment, output handling, OR semantics, missed-run behaviour — pays back the first time you'd otherwise have spent two hours debugging a job that "should have run".