Five fields, and a sixth that is not always there
A classic crontab line is minute (0-59), hour (0-23), day of month (1-31), month (1-12), day of week (0-7), followed by the command. Each field accepts *, a number, a range (1-5), a list (1,15), a step (*/10), or a range with a step (0-30/5). Month and day-of-week also take three-letter names, so 0 9 * * MON-FRI is legal. Sunday is both 0 and 7, which is a convenience that occasionally bites — see below.
Quartz, Spring's @Scheduled, and several cloud schedulers put seconds in front, making six fields. This parser decides by counting. A seven-field form with a trailing year exists in Quartz and is not handled here.
The day-of-month and day-of-week trap
This is the one that produces real incidents. In Vixie cron — the implementation behind Debian, Ubuntu, RHEL and most of what you will meet — if both the day-of-month and day-of-week fields are restricted, they are combined with OR, not AND. So 0 0 13 * 5 does not mean "Friday the 13th". It means "the 13th of every month, and also every Friday", which is roughly sixty-five runs a year instead of one or two.
When one of the two fields is *, the other simply applies and there is no ambiguity. That is the reason to keep one of them * whenever you can. If you genuinely need both conditions, the standard approach is to schedule the broader one and test the narrower one in the script's first line: [ "$(date +\%u)" = 5 ] || exit 0. Note the escaped percent signs — an unescaped % in a crontab command is a newline, which is its own well-loved source of confusion.
Dialects differ, and the differences are not cosmetic
| Implementation | Fields | Notable differences |
|---|---|---|
| Vixie / ISC cron (most Linux) | 5 | DOM/DOW OR rule. @daily shortcuts. % means newline in the command. |
| cronie (RHEL family) | 5 | Vixie plus @reboot, MAILTO, and RANDOM_DELAY on some builds. |
| Quartz (Java) | 6-7 | Seconds first, optional year last. ?, L, W, #. Day-of-week is 1-7 with Sunday = 1, which is off by one from crontab. |
Spring @Scheduled | 6 | Seconds first, no year, no ? requirement. Day-of-week 0-7 like crontab, unlike Quartz. |
| systemd timers | n/a | Not cron at all. OnCalendar=Mon *-*-* 09:30:00, plus persistence and randomised delay that cron has no equivalent for. |
| AWS EventBridge | 6 | Quartz-like, UTC only, and it rejects * in both day fields at once — one must be ?. |
| Kubernetes CronJob | 5 | Standard syntax, but a missed window past startingDeadlineSeconds is skipped entirely rather than caught up. |
The Quartz day-of-week numbering is the one that catches people moving an expression between a Java service and a crontab. 0 0 12 ? * 2 in Quartz is Monday; 0 12 * * 2 in crontab is Tuesday. Nothing errors, the job simply runs on the wrong day, and it takes a week to notice.
Steps are not intervals
*/7 * * * * does not mean "every seven minutes". It means the minutes matching 0, 7, 14, 21, 28, 35, 42, 49 and 56, then the counter resets at the top of the hour — so the gap between the last run of one hour and the first of the next is four minutes, not seven. Any step that is not a divisor of 60 does this. The divisors are 1, 2, 3, 4, 5, 6, 10, 12, 15, 20 and 30; pick from that list and the spacing is uniform.
The same applies to the day field, where */10 gives you the 1st, 11th, 21st and 31st, so a 31-day month has a nine-day tail and February has a nineteen-day one. The gap markers in the next-runs list above make this visible.
Time zones and the two days a year that break scheduling
Cron follows the clock of the host it runs on, which in a container is usually UTC no matter what the host underneath thinks. Check with date on the actual machine, and set CRON_TZ=America/New_York at the top of the crontab if the implementation supports it — cronie and Vixie do, busybox cron does not.
Where a local zone observes daylight saving, two days a year misbehave and it is worth knowing which way. On the spring-forward day, a job scheduled inside the skipped hour does not run at all under most implementations; some versions of Vixie cron compensate by running it once after the jump, which is arguably worse because it is unpredictable. On the fall-back day, the repeated hour can fire a job twice. Anything that moves money, sends mail or truncates a table should either sit outside the 1am-3am window entirely or be idempotent enough not to care. Scheduling at 05:00 UTC and letting local time land where it lands avoids the question completely, which is why so much infrastructure runs on UTC and accepts the mental arithmetic.
Questions people ask
How do I schedule something for the last day of the month?
Standard cron cannot express it, because the last day is not a fixed number. The usual approach is to run daily from the 28th onward and have the script exit unless tomorrow is the first: run on 28-31 and test with [ "$(date -d tomorrow +\%d)" = 01 ]. Quartz has an L character for exactly this, and systemd can do it with OnCalendar=*-*~01 counting backward from the end. If you are on Quartz or systemd, use the native feature; on crontab, test inside the script.
The next run times here do not match what my server does.
Time zone first — cron uses the host clock, this page uses your browser or UTC as selected. Run date on the server and compare. If the zones match, check that the crontab you are reading is the one being used: user crontabs, /etc/crontab and the drop-ins in /etc/cron.d all coexist, and /etc/crontab and /etc/cron.d have an extra user field between the day-of-week and the command that a user crontab does not. Pasting one into the other silently shifts every field by one position.
Can I use a six-field expression with seconds in an ordinary crontab?
No. Vixie cron has one-minute granularity and will refuse the line or misread it as a five-field expression with a stray argument. Six-field expressions belong to Quartz, Spring, and some cloud schedulers. If you need sub-minute work on a plain Linux box, use a systemd timer with OnUnitActiveSec, or have a one-minute cron job launch a loop that sleeps between iterations — and make sure the previous run has exited before the next one starts.
Why did my job not run even though the schedule was right?
The schedule is usually not the problem. In order of frequency: the command failed because cron runs with a minimal environment and a PATH of roughly /usr/bin:/bin, so it could not find a binary that works fine in your shell; the crontab file has no trailing newline on the last line and that line is ignored; an unescaped % in the command truncated it; the user has no mail spool so the error output went nowhere; or the machine was asleep and plain cron, unlike anacron or a systemd timer with Persistent=true, does not catch up on missed runs.