Toolbit
Guides

Crontab: 20 Real-World Examples Explained

Twenty cron expressions for real jobs, from every 5 minutes to month-end, plus the most common traps: the day-field OR rule, unescaped % signs, and daylight saving time.

By Javier VallejoPublished 7 min read

How to read a crontab line

A crontab entry is five time fields followed by the command to run. Each field narrows down when the job fires, and the job runs on every minute where all fields match — with one important exception between day-of-month and day-of-week, covered further down:

┌───────── minute (0–59)
│ ┌─────── hour (0–23)
│ │ ┌───── day of month (1–31)
│ │ │ ┌─── month (1–12)
│ │ │ │ ┌─ day of week (0–7; both 0 and 7 are Sunday)
│ │ │ │ │
* * * * *  command

Every field accepts four ways of writing values, and you can mix them:

SyntaxMeaningExample in the hour field
*any valueevery hour
a,b,clist8,12,18 → at 8, 12, and 18
a-binclusive range9-17 → from 9 through 17
*/n or a-b/nstep*/6 → 0, 6, 12, 18

All 20 examples below stick to that numeric syntax, which every implementation understands — Vixie cron, cronie, BusyBox crond, Kubernetes CronJob. Each expression links to the cron parser with the value already filled in, so you can see the plain-English translation and the upcoming run times.

Short intervals

1. */5 * * * * — every 5 minutes. The classic health check or batch-queue drain. */5 in the minute field means "starting at 0, every 5": 0, 5, 10… 55. It does not mean "5 minutes after the last run finished" — if the job takes 7 minutes, the next one still starts at the next multiple of 5.

2. 0,30 * * * * — every half hour, on the hour and at half past. Same thing as */30 * * * *, but the list spells out the exact minutes, which is easier to read six months later.

3. 0 * * * * — at the top of every hour. Equivalent to the @hourly macro. A very common slip is writing * * * * * and thinking "hourly" — that runs the job every minute, 1,440 times a day.

4. 0 */6 * * * — every 6 hours (00:00, 06:00, 12:00, 18:00). Good for certificate renewals, inventory syncs, or caches that don't need to be minute-fresh. The 0 in the minute field matters: * */6 * * * would run during all 60 minutes of each of those hours.

Daily jobs

5. 30 2 * * * — every day at 02:30. The traditional backup slot: low load, well clear of the date change. It does have a daylight-saving-time catch, covered at the end.

6. 5 0 * * * — every day at 00:05. Five minutes past midnight, for jobs that process "yesterday" — reports, log rotation — and need the day to be fully closed out. Scheduling them at exactly 0 0 tends to collide with the ten other jobs somebody else also put at midnight.

7. 0 8,12,18 * * * — three times a day, at 8, 12, and 18. For example, a digest of open alerts at the start, middle, and end of the workday.

Business hours

8. 0 9-18 * * 1-5 — on the hour from 9 to 18, Monday through Friday. Ranges are inclusive: 9-18 includes 18:00. If the last run should be at 17:00, the range is 9-17.

9. */15 9-17 * * 1-5 — every 15 minutes during business hours. The last run of the day is at 17:45, because the hour field enables all of hour 17 and the minute field sweeps through it.

10. 0 22 * * 1-5 — weeknights at 22:00. For overnight batch work that doesn't need to run on weekends, like compiling the day's sales report.

11. */10 * * * 0,6 — every 10 minutes, Saturdays and Sundays only. The inverse of the previous one: tighter monitoring when nobody's watching the dashboards.

Weekly jobs

12. 0 0 * * 0 — Sundays at midnight. That's the @weekly macro. Sunday can be written as 0 or 7; 0 is the more portable choice.

13. 0 8 * * 1 — Mondays at 08:00. The natural slot for a weekly report someone will actually read when the week starts.

14. 0 3 * * 6 — Saturdays at 03:00. A weekly maintenance window: reindex databases, prune old container images, run fstrim.

Monthly and yearly jobs

15. 0 0 1 * * — the 1st of every month at midnight. That's @monthly. Typical for billing cycles or archiving last month's logs.

16. 0 6 1,15 * * — the 1st and 15th at 06:00. A rough twice-a-month cadence. It isn't "every 15 days": between the 15th and the 1st of the next month there can be anywhere from 14 to 17 days, depending on the month.

17. 15 14 1 * * — the 1st of every month at 14:15. This is the example from the crontab(5) man page itself, and it's a handy reminder of field order: minute first, then hour.

18. 0 0 1 1,4,7,10 * — the first day of each quarter. January, April, July, and October. You can also write it as 0 0 1 */3 *, because a step starts from the field's minimum (1) and yields 1, 4, 7, and 10.

Two trick cases

19. 0 4 8-14 * 1 — this is NOT "the second Monday of the month." It looks right, since the second Monday always falls between the 8th and the 14th. But when day-of-month and day-of-week are both restricted, cron combines them with OR, not AND: the job runs every day from the 8th through the 14th, plus every Monday. The fix is to restrict only the day of month and check the weekday inside the command:

0 4 8-14 * *  [ "$(date +\%u)" = 1 ] && /usr/local/bin/job

The % signs are escaped with \ because in a crontab an unescaped % is treated as a newline — everything after it gets fed to the command on standard input.

20. 0 23 28-31 * * plus a check — the last day of the month. Cron has no standard way to say "last day" (some implementations accept L, but it isn't portable). The trick is to run on the 28th through the 31st and check whether tomorrow is the 1st:

0 23 28-31 * *  [ "$(date -d tomorrow +\%d)" = "01" ] && /usr/local/bin/month-end-close

date -d tomorrow is GNU date syntax; on BSD and macOS the equivalent is date -v+1d.

What the crontab doesn't tell you (but should)

  • The environment is bare-bones. Cron runs with a stripped-down PATH (often just /usr/bin:/bin) and with /bin/sh, not your login shell. Use absolute paths, or set PATH= at the top of the crontab.
  • Output gets mailed — or vanishes. With no mail transfer agent configured, anything the command prints is thrown away. Redirect explicitly to avoid surprises: >> /var/log/job.log 2>&1.
  • Runs can overlap. If a job scheduled every 5 minutes sometimes takes 8, you'll have two copies running at once. flock -n /tmp/job.lock command makes the second copy exit without running.
  • The time zone is the system's. A server on UTC runs 0 9 * * * at 09:00 UTC, not 09:00 local. Cronie lets you override it with the CRON_TZ variable. For the full story on local time, UTC, and timestamps, see Unix Timestamps and Time Zones: Handling Dates Without Bugs.
  • Daylight saving time shifts things. On changeover day, one local hour either doesn't exist or happens twice. Cronie and Vixie cron try to compensate for fixed-time jobs, but a 02:30 job can still land at an unexpected time that day. Keep critical jobs out of the 01:00–03:00 window, or run the server on UTC.
  • Macros save keystrokes, not clarity. @daily, @weekly, @monthly, and @yearly stand for 0 0 * * *, 0 0 * * 0, 0 0 1 * *, and 0 0 1 1 *. @reboot (run at startup) exists in Vixie cron and cronie, but it isn't a schedule, so there are no "next runs" to calculate.

Summary

Nearly any real-world schedule fits into five fields using lists, ranges, and steps. The costly mistakes always come from the same places: a * in the minute field that turns "hourly" into "every minute," the OR rule between day-of-month and day-of-week, unescaped % signs, and an environment that isn't the one in your terminal. Before installing an expression in production, paste it into the parser and look over the list of upcoming runs — it's the fastest way to catch any of these.

Tools used in this guide

Related guides