Notes › Tech Notes › Crontab expressions explained, with predicted run times

Crontab expressions explained, with predicted run times

At the end of the year I inherited a server from a colleague who had left, and the ops team complained that the month-start reconciliation email arrived three times a week. I looked at the crontab and found a single line, 0 9 1 * 1; my first reading was the 9 a.m. slot that is both the 1st of the month and a Monday. Only when I sorted the run log by time did I see the problem: it ran on the 1st of every month, and it also ran every Monday. In cron, when both the day-of-month and day-of-week fields are restricted, the relationship is or. One misread meaning became a weekly duplicate send. That episode made me go through cron syntax again, and the following is what actually bites.

1. The five fields are minute, hour, day, month and weekday, and the ranges are narrower than you think

Read left to right, one space per field, in a fixed order: minute, hour, day of month, month, day of week. Many people overthink the precedence between day of month and day of week; they are simply two independent filters.

FieldRangeWhat * meansEasy to misread
Minute0–59Every minuteThere is no 60; 60 * * * * is invalid
Hour0–23Every hour0 is midnight, not "none"
Day of month1–31Every dayIt starts at 1, not 0
Month1–12Every monthNot 0–11, unlike some programming languages
Day of week0–7Every day of the week0 and 7 both mean Sunday; 1 is Monday

So 0 2 * * * is every day at 02:00, and 30 6 * * 1-5 is 06:30 on weekdays. When reading an expression I look at the last two fields first: whenever neither day of month nor day of week is *, read it as or. That is the rule that most often contradicts intuition.

2. Four symbols, and */5 is the one most often misunderstood

  • * any value; , a list, so 0,15,30,45 is four fixed moments within the hour.
  • - a range, so 9-18 is a closed interval including both endpoints, 9 through 18.
  • /n a step. The key point: cron does not wait n minutes after the last run; it picks values that divide evenly on a fixed time grid. */5 * * * * lands on minutes 0, 5, 10 and so on through 55 of every hour, always firing on the hour; */7 * * * * lands on 0, 7, 14, 21, 28, 35, 42, 49 and 56, then restarts at 0 in the next hour, so the gap between runs jumps between 4 and 7 minutes instead of staying at 7. That is why the requirement every 7 minutes can never be written accurately.
  • A step can carry a start point: 5-59/10 means minutes 5, 15, 25, 35, 45 and 55, useful for staggering jobs away from the top of the hour when everyone else runs at :00.

3. How I confirm an expression is correct

  1. First write the requirement as a plain sentence on paper, specific enough to say which day at which hour and minute, how many times a week and how many times a month, with concrete counts.
  2. Then translate it into an expression and drop it into Crontab Parser to see what each field is interpreted as, checking especially whether the combination of day of month and day of week is the one you meant.
  3. Look at the next 10 predicted run times and read the weekday and time aloud one by one: if I asked for Wednesday at 03:30, every prediction must be a Wednesday, and the gap must be 7 days, not 6 or 8.
  4. Finally, check the line that was written with crontab -l: make sure nothing like set -e sits at the start of the line and gets treated as a job, and that there are no stray tabs.

The prediction step cannot be skipped. When logs go wrong, what time it actually ran is the only useful evidence, and the expression itself will not tell you.

4. Four more details that trip people up

  • Time zone: cron interprets every field in the server's local time zone. On a UTC machine, 0 3 * * * corresponds to 11:00 Beijing time, right in the middle of the business peak. To check, compare the output of date +%s and date, then use Timestamp Converter to expand the same number in UTC and in local time; the 8-hour offset is obvious at a glance. Newer implementations such as cronie also accept CRON_TZ or TZ at the top of the file to set the time zone for a job, but not every cron version honors those variables, so confirm on a test machine first.
  • Percent signs: in a crontab, % means newline, with the rest passed as standard input, so date +%Y%m%d must be written date +\%Y\%m\%d; otherwise the job fails silently.
  • Environment variables: cron hands out a narrow PATH, usually just /usr/bin:/bin, so a script calling node, python3 or aws often reports command not found while running fine by hand. The safe fix is to load the environment explicitly on the first line of the script, or to use absolute paths throughout.
  • Output nobody collects: when a job produces output, cron tries to mail it; most servers have no MTA, so it looks as if nothing happened. Add >> /var/log/cron_backup.log 2>&1 and turn off MAILTO="", and at least the timestamps leave a trail.

5. Commonly copied expressions and how often they really fire

ExpressionMeaningRuns per day
* * * * *Every minute1440
*/15 * * * *Every 15 minutes (:00 :15 :30 :45)96
5-59/10 9-18 * * *Minutes 5, 15, 25, 35, 45 and 55 of every hour from 9 to 1860
30 3 * * 03:30 a.m. every Sunday0.14 on average
0 2 1 * *02:00 on the 1st of each month0.033 on average
0 9 * * 1-509:00 on weekdays0.71 on average
0 9 1 * 109:00 on the 1st of each month or every MondayAbout 0.17 (roughly 62 times a year)

The last row is the one that got me. If you truly need the first Monday of each month, the cron fields cannot express it; the script has to check [ $(date +%d) -le 7 ] when it fires and exit immediately otherwise.

6. Common questions

Q: Can cron run jobs by the second?

A standard crontab has 5 fields, so the smallest granularity is 1 minute and seconds are out of reach. For higher frequency, people run an inner loop under * * * * *, or switch to a scheduler with 6 or 7 fields including seconds and year, but that syntax is not fully compatible with cron.

Q: Should Sunday be written as 0 or 7?

Either. Both 0 and 7 mean Sunday. A team should pick one; mixing them makes ranges like 1-7 and 0-6 hard to reason about.

Q: Do I need to restart the service after editing crontab?

Not if you edit with crontab -e; the daemon reloads automatically after you save. When editing /etc/crontab or files under /etc/cron.d/ directly, watch the owner and permissions; overly broad permissions cause the file to be ignored.

Q: Why does a script run fine by hand but fail under cron?

Nine times out of ten it is an environment difference: a short PATH, HOME unset, relative paths resolved against $HOME, the working directory not being the script directory, or a non-interactive shell not loading your .bashrc. To diagnose it, run env > /tmp/cron_env.txt inside the job and compare it with the env output from your shell; the difference shows up immediately.

Q: If a job hangs and does not finish, will the next run still fire?

Yes. Cron does not care whether the previous run finished, so the same job can overlap with itself. Backups of unpredictable duration need their own lock; the common approach is to wrap the command in flock and exit immediately if the lock is held.

Looking back at those three duplicate reconciliation emails, the problem was not the server; it was that I read the relationship as and when it was or. Writing an expression is only half the work; the other half is confirming which moments it actually lands on. Write the requirement as one sentence first, then check it against the predicted run times; that beats reading the documentation.

📎 Related articles

PNG compressed three times only lost 426 bytes: the real boundary of lossless compression 2026-10-22 How Many Times Bigger a PNG Gets After Converting From WebP: When It Is Unavoidable and How to Keep the Growth Down 2026-10-22 Three Reasons the Small Text Smudges When a Screenshot Is Shrunk to a Quarter 2026-10-22 Cropped to the Same 300x300, Why Does the Avatar Always Sit Half a Cell Low-Right 2026-10-22 Forecasting 18 months at 1.7 times a month: why the script and the sheet give two numbers 2026-10-22