cron Cheatsheet - Cron Expression & Job Scheduling

The syntax of a scheduled job is five fields plus three symbols, learnable in ten minutes. What actually blocks people is the environment and the ownership after the job starts. Cron does not run in your terminal environment: PATH usually contains three directories, so every alias and every path added by pyenv or nvm is missing, and the job silently produces nothing or reports command not found. The second trap is user context - a job in /etc/cron.d/ runs with the permissions of that file, a job in a crontab runs as the crontab owner, and the same command behaves differently in each place. This sheet covers field syntax and shortcuts such as @daily and @reboot, the system-level directories, user-level crontab commands, environment configuration, output redirection and a debugging order. Reach for it when a job is confirmed to have fired but produced no artefact: grep syslog to confirm dispatch, then check the daemon with systemctl status cron, and if both are empty the crond daemon itself never ran. Always redirect to a file explicitly, because MAILTO is empty on most cloud machines.

SysOps·43 commands·Last updated 2026-07-21
cronScheduled TaskscrontabAutomation

Typical Use Case

For ops and backend engineers who need a job to run when nobody is watching. Four cases dominate: nightly backups and monthly cleanup, a five-minute health probe that catches a dead local process before outside monitoring does, a weekday 9am digest written as 0 9 * * 1-5, and @reboot bringing a daemon back after a reboot. The format is the easy part; the traps are an almost empty environment, silent failures because output goes to MAILTO, and the system time zone.

Schedule Format 7

* * * * * command
Format: min hour day month weekday
*/15 * * * *
Every 15 minutes (step)
0 */2 * * *
Every 2 hours
0 18 * * 0-6
Daily at 18:00 (0-6 range)
10 2 * * 6,7
Sat/Sun at 2:10 (comma list)
0 0 * * 0
Every Sunday at midnight
0 0 1 * *
Monthly on the 1st at midnight

Special Strings 6

@reboot
Run once at boot
@hourly
Hourly (same as 0 * * * *)
@daily / @midnight
Daily (same as 0 0 * * *)
@weekly
Weekly (same as 0 0 * * 0)
@monthly
Monthly (same as 0 0 1 * *)
@yearly / @annually
Yearly (same as 0 0 1 1 *)

System Cron 6

/etc/crontab
System crontab (adds a user column)
/etc/cron.d/
Drop-in task files (same format)
/etc/cron.hourly/
Hourly scripts dir
/etc/cron.daily/
Daily scripts dir
/etc/cron.weekly/
Weekly scripts dir
/etc/cron.monthly/
Monthly scripts dir

User Crontab 7

crontab -e
Edit the current user's crontab
crontab -l
List the current user's jobs
crontab -r
Remove all jobs for the user
crontab -ri
Remove with confirmation prompt
crontab -u user -e
root edits another user's crontab
crontab -u user -l
View another user's jobs
echo "@reboot echo hi" | crontab
Pipe in jobs quickly

Environment 6

SHELL=/bin/bash
Set the shell cron uses
PATH=/usr/local/bin:/usr/bin:/bin
Set PATH (cron's default is minimal)
MAILTO="admin@example.com"
Mail output to an address
MAILTO=""
Disable mail (no output sent)
HOME=/root
Set the HOME dir
CRON_TZ=Asia/Shanghai
Set the cron timezone

Examples 6

0 2 * * * /path/backup.sh
Nightly backup at 2am
*/5 * * * * curl -s https://example.com/health
Health check every 5 minutes
0 9 * * 1-5 mail -s "Report" team@example.com
Email on weekdays at 9am
0 0 1 * * /path/cleanup.sh
Clean temp files monthly
30 3 * * * /usr/bin/logrotate /etc/logrotate.conf
Rotate logs daily at 3:30
0 0 * * 0 /path/weekly-report.sh >> /var/log/report.log 2>&1
Weekly report with logging

Debugging 5

grep CRON /var/log/syslog
Cron log on Debian/Ubuntu
grep cron /var/log/cron
Cron log on RHEL/CentOS
date
Verify the system timezone
systemctl status cron
Check the cron service status
* * * * * echo test >> /tmp/cron-test 2>&1
Debug: write a test line every minute

Parameter matrix

ParameterEffectExample
* * * * *The five fields are minute, hour, day of month, month and day of week, firing once a minute; this is the standard probe for whether cron is alive at all* * * * * command
*/15 * * * *Step syntax, usable per field; */15 on the minute means every 15 minutes, which is far easier to keep correct than four separate */5 lines*/15 * * * *
0 */2 * * *A step on the hour field fires every 2 hours; note that this means 0, 2, 4 ... 22 rather than two hours counted from the current instant0 */2 * * *
0 18 * * 0-6A range plus a comma-separated list of days; field values run 0-6, where Sunday is either 0 or 70 18 * * 0-6
10 2 * * 6,7A comma-separated list of values, 02:10 on both Saturday and Sunday; harder to duplicate or silently skip than two separate rules10 2 * * 6,7
0 0 1 * *A day-of-month of 1, meaning midnight on the first of every month, the usual shape for monthly jobs0 0 1 * *
@rebootRuns once after boot, which makes it the simplest replacement for rc.local when you only want something to come back up automatically@reboot
@hourly / @daily / @weekly / @monthlyPreset macros equivalent to 0 * * * *, 0 0 * * *, 0 0 * * 0 and 0 0 1 * * respectively; @midnight and @annually are synonyms@daily
SHELL=/bin/bashPicks the shell that runs the command; without it /bin/sh is used, and sh knows neither source, [[ ]] nor pipefailSHELL=/bin/bash
PATH=/usr/local/bin:/usr/bin:/bincron ships a minimal PATH that omits nvm, pyenv and homebrew, which is why node or python so often report not foundPATH=/usr/local/bin:/usr/bin:/bin
CRON_TZ=Asia/ShanghaiSets the timezone for one crontab and takes precedence over the system TZ, the only dependable option for a team spread across zonesCRON_TZ=Asia/Shanghai
MAILTO=""Discards cron mail output so each run stops sending you an empty messageMAILTO=""
crontab -e / crontab -lEdits or lists the current user crontab; nothing reloads until you save out of crontab -ecrontab -e
crontab -r / crontab -ri-r deletes the whole crontab with no confirmation while -ri asks first, and a mistaken -r cannot be undonecrontab -ri
crontab -u user -e / -lManages another user crontab as root, which is the only way to change jobs owned by a vip or a sudo usercrontab -u user -l
/etc/crontab and /etc/cron.d/The system-wide locations; a file under /etc/cron.d/ must be owned by a non-login user, be mode 644, and carry a user column/etc/crontab
>> /var/log/report.log 2>&1Redirects both stdout and stderr into a file, which remains the single most effective way to answer whether cron ran at all0 0 * * 0 /path/weekly-report.sh >> /var/log/report.log 2>&1

Common pitfalls

SymptomA job written in crontab -e runs fine by hand but never fires, and nothing is logged.

CauseThe five fields are misordered (month written first) or the separators mix spaces and tabs, so cron silently drops the line; an environment difference is the other suspect, since an interactive shell has a PATH and cron does not.

FixFirst add * * * * * echo test >> /tmp/cron-test 2>&1 to prove the scheduler itself works, then debug the real line; declare SHELL and PATH explicitly at the top of the crontab.

SymptomA job set for 2am on macOS fires some time in the afternoon instead.

Causecron interprets the fields in the system local timezone, which is not what you assumed; on macOS a sleeping laptop also defers jobs until it wakes.

FixConfirm the zone with date and declare CRON_TZ=Asia/Shanghai in the crontab, then cross-check the result against the date output.

SymptomYou assume 0 0 * * 7 means every Sunday, and the job either doubles up with other Sunday work or never runs.

CauseIn the day-of-week field both 0 and 7 mean Sunday, so 7 and 0 are the same rule; more often than not Sunday is mistaken for 1, which is Monday.

FixUse 0 consistently for Sunday: 0 0 * * 0, and 0 0 * * 6,0 when you want Saturday and Sunday.

SymptomA script calling node, python or gcloud reports command not found although the same command works in the terminal.

Causecron PATH is only /usr/bin:/bin, so anything installed by nvm, pyenv, homebrew or asdf is missing from it.

FixWrite the full PATH at the top of the crontab, or call the absolute path straight in the rule, for example /usr/local/bin/node; sourcing an env script is the most robust of all.

Symptomcrontab -r was used to clear a test job, and the whole production crontab is gone the next day.

Causecrontab -r confirms nothing and deletes every job for the current user without keeping a backup.

FixSwitch to crontab -ri by habit, and snapshot with crontab -l > ~/crontab.bak before editing.

SymptomA log cleanup job in crontab never cleans anything and instead litters the disk with zero-byte files.

Causelogrotate depends on its state file under /etc/logrotate.conf and on the running user; calling it by hand from cron bypasses the rotation checks, so it can rotate twice or target the wrong path.

FixLet logrotate own it through its timer or cron entry, using 30 3 * * * /usr/bin/logrotate /etc/logrotate.conf, and make sure the script ends with exit 0.

SymptomA new file under /etc/cron.d/ simply never executes.

Cause/etc/cron.d/ demands an owner other than root, mode 644, and a user field on every line; mode 600 or a missing user column makes cron reject the file outright.

FixRun chmod 644 on the file, give it a non-root owner, and append the user column to the schedule line, for example * * * * * deploy /path/x.sh.

SymptomTwo jobs start almost simultaneously, fight over the same file or port, and one of them occasionally fails.

CauseAdjacent time fields let both jobs land inside the same minute, with no mutual exclusion between them.

FixOffset the minute fields, say 0 2 and 5 2, or take an exclusive lock at the top of the script: flock -n /var/lock/backup.lock /path/backup.sh.

Troubleshooting

  1. 1Prove the scheduler is alive with a one-minute probe

    * * * * * echo test >> /tmp/cron-test 2>&1

    After a minute, look for a new line in /tmp/cron-test; if there is none, investigate the daemon rather than your script.

  2. 2Confirm the cron service is running

    systemctl status cron

    The unit is cron on Debian/Ubuntu and crond on RHEL/CentOS; nothing is parsed while it is stopped.

  3. 3Read the system log for cron load and run records

    grep CRON /var/log/syslog

    CRON scheduling lines mean the rule was parsed; total silence means the syntax was rejected or the file was never read.

  4. 4Where the log lives on RHEL/CentOS

    grep cron /var/log/cron

    The same information lands in this file on RHEL systems instead of /var/log/syslog.

  5. 5Check that the machine timezone matches the one you intended

    date

    cron runs in the system local zone; if that disagrees with a CRON_TZ=Asia/Shanghai declaration then every job on the box is shifted.

  6. 6List and back up the current user crontab

    crontab -l

    Empty output means the user has no jobs at all; snapshot with crontab -l > ~/crontab.bak before changing anything.

Command Examples

A backup job with logging and stderr redirect

0 2 * * * /path/backup.sh >> /var/log/backup.log 2>&1 crontab -l

cron runs with a minimal environment, so PATH, HOME and locale variables can all be missing inside the script; prefer absolute paths everywhere. >> appends and 2>&1 folds stderr into the log — without it a failure is silent and all you observe is "the job did not run".

Prove cron is alive with a throwaway job

* * * * * echo test >> /tmp/cron-test 2>&1 sleep 65 cat /tmp/cron-test rm -f /tmp/cron-test

The first step of any "my job never runs" investigation: install a one-line every-minute job to separate "cron is dead" from "my command is wrong". Once that passes, go debug the real script and avoid burning time in the wrong direction.

Output

test

Setting PATH, SHELL and CRON_TZ at the top of the crontab

SHELL=/bin/bash PATH=/usr/local/bin:/usr/bin:/bin CRON_TZ=Asia/Shanghai MAILTO=""

The default cron PATH is often just /bin:/usr/bin, so your locally installed go, node or python are invisible — the classic cause of "works when I run it, command not found under cron". CRON_TZ keeps cross-timezone schedules off the server clock, and MAILTO="" disables the mail noise.

Output

* * * * * /usr/bin/logrotate /etc/logrotate.conf

Set a job quickly through a pipe

echo "@reboot /path/start-app.sh" | crontab crontab -l

The crontab command reads replacement content from stdin, so echo | crontab overwrites the entire existing schedule. That makes it ideal for scripted setup, but run crontab -l first and keep a copy — it is the easiest way to lose every job inside an automation script.

Drop a standalone job file into /etc/cron.d

SHELL=/bin/bash PATH=/usr/local/bin:/usr/bin:/bin 0 3 * * * root /usr/local/bin/nightly.sh >> /var/log/nightly.log 2>&1

Files in /etc/cron.d take one extra column: the user name. Omit it and cron silently ignores the whole file, without a warning. One job per file is the convention; a non-standard suffix keeps cron from running the file, and it must be mode 644 owned by root.

Common Pitfalls

  • In crontab, % is not an ordinary character — it means newline. An unescaped % truncates the command and the job fails silently, so a date format must be written date +\%Y-\%m-\%d, and any path containing % needs the same escaping.
  • crontab -r deletes every scheduled job for the current user with no confirmation and no trash. crontab -ri at least asks once; inside automation, back up with crontab -l > backup.txt first.
  • Every % must be escaped, and the handling of redirects and && varies between cron implementations. The dependable pattern is to move the whole command into a script and leave one absolute path in the crontab.
  • This sheet covers crontab, the scheduled-job command; the cron daemon itself is a separate concern (systemctl status cron). Since both appear across the cron and crontab sheets, keep them apart: one is the service, the other is the editor you use for user schedules.
  • cron runs on the server clock, so containers, VMs and DST transitions can shift a job by an hour. Pin the zone with CRON_TZ, or confirm with date first.
  • Jobs missed during a reboot are not replayed without something like anacron. At the same time, with MAILTO still set, every stdout line becomes an email — a few hundred log lines can flood a mailbox or trip a rate limit.

Tips

  • Format: min(0-59) hour(0-23) day(1-31) month(1-12) weekday(0-6, 0=Sun).
  • * = all, , = list, - = range, / = step.
  • Use absolute paths in scripts or commands may not be found.
  • cron's env is minimal; set PATH explicitly in scripts.
  • Redirect output: command >> /var/log/cron.log 2>&1.
  • Sun = 0 or 7 depending on distro; standardize on 0.

FAQ

My crontab entry never fires — what should I check first?

Work down this list in order. First, is the daemon alive: systemctl status cron should show it running. Second, did the entry actually get saved: crontab -l lists the current user's jobs, and empty output means it never persisted (quitting the editor without saving loses it just as surely). Third, did the scheduler even attempt it — grep CRON /var/log/syslog on Debian/Ubuntu or grep cron /var/log/cron on RHEL/CentOS shows the exact minute each command was invoked. Fourth, run date: cron follows the system timezone, and a mismatch with your own clock is the classic reason a job looks like it "never ran".

Why does the command work by hand but fail under cron?

The cause is cron's minimal environment: it is not a login shell, so your .bashrc or .zshrc never loads, PATH is often a couple of system directories, and HOME and SHELL may not be what you assume. The result is that scripts calling node, python, curl or git from non-standard locations cannot find them, or see empty variables. Two fixes: write every command and script path absolutely, and declare the environment at the top of the crontab — SHELL=/bin/bash, PATH=/usr/local/bin:/usr/bin:/bin, HOME=/root. Setting MAILTO="" while debugging also stops cron from emailing away the output that would have told you what went wrong.

The job runs but leaves no trace — how do I capture the output?

cron mails task output by default, so unless you can read that mailbox you see nothing. Redirect explicitly: append >> /var/log/cron.log 2>&1, where >> preserves previous runs and 2>&1 folds stderr into the same file so crash reasons are captured too. Place it at the end of the command, e.g. 0 0 * * 0 /path/weekly-report.sh >> /var/log/report.log 2>&1. Log rotation can then be scheduled the same way with 30 3 * * * /usr/bin/logrotate /etc/logrotate.conf. To test the scheduling chain in isolation, add the minimal probe * * * * * echo test >> /tmp/cron-test 2>&1 and check one minute later whether the file grew.

My schedule is right but always fires an hour off — why?

cron interprets times in the server's system timezone, not yours. Run date to check it: on a UTC server with you on UTC+8, "every day at midnight" means 00:00 UTC, which is 08:00 local. Two remedies: set the server timezone to match yours, or pin CRON_TZ=Asia/Shanghai at the top of the crontab so cron uses your zone without touching the system clock — important because other programs on the box keep depending on system time. The shortcut strings @daily and @midnight are affected identically: they mean midnight in that same configured zone.

How do I express "daily" and "weekdays only" in the five cron fields?

The format is five columns — minute, hour, day-of-month, month, day-of-week — ranging 0-59, 0-23, 1-31, 1-12 and 0-6 where 0 is Sunday. "Daily at 02:00" is 0 2 * * *, pinning the first two fields and letting * cover every day, month and weekday. "Weekdays at 09:00" is 0 9 * * 1-5, where the last field restricts execution to Monday through Friday. Hourly is 0 * * * *, or the equivalent @hourly. Disjoint values are comma-separated, so 10 2 * * 6,7 means 02:10 on Saturday and Sunday. Steps use a slash: */15 * * * * runs every 15 minutes and 0 */2 * * * every two hours. Stick to 0 for Sunday rather than mixing in 7.

Official References

Each command links to its official documentation below, so you can verify the latest usage and read deeper.

Maintained by LaoHand

Publicly updated on Jul 21, 2026, continuously proofread against official docs.

Contact Us

Wrong command or description? Send us corrections, business inquiries or product feedback by email.

Contact Us