The five fields
A crontab is a table of schedules read by the cron daemon (in practice, crond). Each line has five time fields followed by the command to run. The fields are, in order: minute, hour, day-of-month, month and day-of-week. The daemon wakes once a minute, compares the clock against every line and runs the command when the fields match. The base specification is POSIX (The Open Group), but almost every Linux runs a Vixie cron variant (maintained today as cronie), which adds handy extensions, the @daily shortcuts, month names and the value 7 for Sunday.
┌───────────── minute (0-59)
│ ┌─────────── hour (0-23)
│ │ ┌───────── day of month (1-31)
│ │ │ ┌─────── month (1-12)
│ │ │ │ ┌───── day of week (0-7, 0/7 = Sun)
│ │ │ │ │
* * * * * command to run| Field | Range | Special values/names |
|---|---|---|
| Minute | 0–59 | — |
| Hour | 0–23 | — |
| Day of month | 1–31 | — |
| Month | 1–12 | names JAN–DEC |
| Day of week | 0–7 | 0 and 7 = Sunday; names SUN–SAT |
Day-of-week is the quiet gotcha. Under strict POSIX, the range is 0–6 with 0 = Sunday. Vixie cron extends it to 0–7 and makes **both 0 and 7 mean Sunday**, a courtesy for people who expect the week to end on 7. This matters when you write 1-5 (Monday to Friday) or 6,0 (the weekend). Month and day names (JAN, SUN) are also a Vixie extension, not POSIX; on a minimal system, prefer the numbers. The Cron Generator treats 7 as Sunday and validates the ranges field by field, so it is a good place to confirm that sun and 0 produce the same schedule.
The operators and the step
Each field takes four constructs. The asterisk matches every value; the comma lists individual values; the hyphen defines an inclusive range; and the slash applies a step, skipping by N within a range (or within an asterisk, which stands for the field's full range). POSIX defines only *, , and -; the / step is a Vixie extension, one more reason not to assume every expression runs everywhere.
| Operator | Meaning | Example | Result |
|---|---|---|---|
| * | all values of the field | * * * * * | every minute |
| , | list of values | 0 0,12 * * * | at 00:00 and 12:00 |
| - | inclusive range | 0 9-17 * * * | hourly from 09 to 17 |
| / | step within a range | */15 * * * * | at minutes 0, 15, 30, 45 |
**Worked example.** Take */15 9-17 * * 1-5, every 15 minutes, from 9am to 5pm, Monday to Friday. Here only day-of-week is restricted (day-of-month is *), so there is no trap: it is a clean AND between the time fields and the weekdays. Starting from a Thursday, 2026-07-09, at 16:07, the next fires are: 16:15, 16:30, 16:45, 17:00, 17:15, 17:30 and 17:45 (hour 17 is inside the 9-17 range). After 17:45 the window closes; the next is Friday 2026-07-10 at 09:00. The weekend is skipped, so after Friday the job only returns on Monday 2026-07-13 at 09:00.
The OR trap: day-of-month and day-of-week
Here is the rule that breaks everyone's intuition. You read the expression left to right and assume every field has to match at once, one big AND. That holds for minute, hour and month. But day-of-month and day-of-week get special treatment: when **both** are restricted (neither is *), cron fires when **either one** matches. It is an OR, not an AND. Vixie's crontab(5) states, verbatim: "If both fields are restricted (i.e., do not contain the '*' character), the command will be run when either field matches the current time." POSIX describes the same semantics in other words.
There is no way to express "Friday the 13th" in a single POSIX cron field, precisely because of the OR. The idiomatic fix is to schedule for every 13th, 0 0 13 * *, and test the weekday inside the command itself, e.g. [ "$(date +\%u)" = "5" ] && /path/script. Since only day-of-month is restricted (day-of-week is back to *), the OR rule does not kick in: cron fires on every 13th and test decides whether that 13th is a Friday. The practical rule is simple: if you need both day fields to act together as an AND, leave one of them as * and move the other condition into an if in the script.
Shortcuts: @daily, @reboot and friends
Instead of the five fields, Vixie cron accepts nicknames starting with @. They are not part of POSIX, but they exist on practically every Linux and make the intent obvious. Each nickname maps to a five-field expression, except @reboot, which has no equivalent because it means "once, when cron starts".
| Nickname | Equivalent to | When it runs |
|---|---|---|
| @yearly / @annually | 0 0 1 1 * | January 1st, 00:00 |
| @monthly | 0 0 1 * * | 1st of each month, 00:00 |
| @weekly | 0 0 * * 0 | every Sunday, 00:00 |
| @daily / @midnight | 0 0 * * * | every day, 00:00 |
| @hourly | 0 * * * * | every hour, at minute 0 |
| @reboot | (no equivalent) | once, when cron starts |
Two details: @midnight is a synonym for @daily in cronie's code, even though the current manual only documents @daily; and @reboot runs when the **daemon** starts (typically at boot), not at every login, if the service restarts on its own, the job fires again. Two questions that keep coming back, how cron handles daylight saving time and what exactly counts as a "reboot", deserve a little more room.
Cron and daylight saving time (DST): skipped and repeated jobs
Cron runs in the time zone of the system where the daemon lives, and DST transitions shift the local clock. The cron(8) manual describes special handling for jumps of less than 3 hours (the DST case). In spring, when the clock moves forward and an hour "disappears", jobs that would have run in the skipped hour are run soon after the change. In autumn, when the clock moves back and an hour repeats, jobs that fall in the repeated hour are not run again. This applies to specific-time schedules; wildcard jobs (e.g. * * * * *) run normally over the new time.
Changes of more than 3 hours are treated as clock corrections, and the new time takes effect immediately. The operational lesson: for time-zone-sensitive tasks, run the daemon in UTC or avoid the transition window (somewhere between 1am and 3am, depending on the country). If you need to reason about these jumps, the time zones, UTC and daylight saving guide details why "02:30" may not exist or may exist twice, and the time zone converter helps align the server's time with your audience's.
What exactly @reboot runs
@reboot fires exactly once, when the cron service starts. On most systems that coincides with the machine booting, but it is not guaranteed: if you restart only the daemon (for example, systemctl restart cron), the @reboot jobs run again, without the machine having rebooted. There is no time attached either, it does not "wait" for a moment; it runs as soon as cron starts and reads the crontab.
That is why @reboot is fragile as a service-startup mechanism. If the goal is to bring a process up with the system, a systemd service (with dependencies, automatic restart and boot ordering) is more robust. Reserve @reboot for idempotent, cheap tasks, clearing a temp directory, rebuilding a cache, where running again after a daemon restart does no harm.
Not every "cron" is POSIX: Quartz and Spring
A lot of the expressions floating around the internet do not run in a Linux crontab, they are Quartz, the Java-world scheduler, or the format Spring uses. The most visible clue is the field count: Quartz uses six (seconds, minute, hour, day-of-month, month, day-of-week) plus an optional seventh for the year; Spring uses six, with seconds up front. If you count six numbers where you expected five, you are probably looking at an expression with a seconds field, pasting it into a system crontab makes cron complain or misread everything.
POSIX / Vixie cron (Linux)
- 5 fields: minute, hour, day-of-month, month, day-of-week.
- Day-of-week 0–7 (0 and 7 = Sunday).
- Operators *, ,, - and (in Vixie) /.
- Both day-of-month and day-of-week restricted = OR.
- No seconds, no ?, L, W, #.
Quartz / Spring (Java)
- 6 fields (seconds up front) + optional year in Quartz.
- Day-of-week 1–7 (1 = Sunday, 7 = Saturday).
- ? = "no specific value" in one of the day fields.
- L = last day; W = nearest weekday.
- # = nth weekday of month (6#3 = 3rd Friday).
Quartz solves the OR trap differently: it **forbids** specifying day-of-month and day-of-week at the same time. You must put ? in one of the two, making explicit which day rules. The documentation states you "must currently use the '?' character in one of these fields". That is why Quartz expressions often carry a ? where Linux cron would have a *, and why 6#3 (third Friday of the month), which Quartz packs into a single field, needs a script-side test under POSIX cron. When migrating between the two worlds, rewrite the expression from intent, do not copy the symbols: beyond the seconds field and the ?, the day-of-week numbering shifts (0 = Sunday on Linux, 1 = Sunday in Quartz).
Ready-made recipes
Most everyday schedules fit into half a dozen patterns. The table below lists verified five-field expressions, with what each one does. All of them dodge the OR trap: whenever the two day fields could collide, one of them stays as *.
| Expression | What it does |
|---|---|
| 0 0 * * * | Every day at midnight (same as @daily). |
| */15 * * * * | Every 15 minutes (at minutes 0, 15, 30, 45). |
| 0 9 * * 1-5 | At 09:00, Monday to Friday. |
| 0 */6 * * * | Every 6 hours (00:00, 06:00, 12:00, 18:00). |
| 0 0 1 * * | 1st of each month, 00:00 (same as @monthly). |
| 0 0 * * 0 | Every Sunday at midnight (same as @weekly). |
| 23 0-23/2 * * * | At minute 23 of every even hour (00:23, 02:23, …, 22:23). |
| 30 3 1 1 * | At 03:30 on January 1st, every year. |
One last hygiene note: always schedule with a fallback for timing. If the server is off at the exact moment, plain cron simply misses the fire (anacron covers part of this on desktops). And instrument the job, a cron that runs but fails silently is worse than one that never runs. A cheap pattern is for the job to hit a "heartbeat" endpoint at the end; if the heartbeat does not arrive, you get alerted. To read what the server returns on that heartbeat, the essential HTTP status codes guide separates the 2xx that confirms success from the 5xx that signals failure. And when the job writes Unix timestamps to its logs, the timestamp converter turns those numbers into readable dates while you debug.
Frequently asked questions
Why does my "Friday the 13th" cron run on other days?
Does */5 mean "every 5 minutes from now"?
0 or 7 for Sunday, which one?
Why does a 6-field expression not work in my crontab?
Does cron respect daylight saving time?
Cron is five fields and four operators, but two details trip up anyone trusting intuition: */n counts from the field's zero, not from "now", and day-of-month with day-of-week become an OR when both are restricted. Master those two points, learn to tell POSIX cron from the Quartz extensions, and always validate by the next fires before trusting a schedule.