The five fields, in order

A standard cron expression is five whitespace-separated fields. The order is fixed and rarely written down where you can find it in a hurry:

┌──────── minute       (0-59)
│ ┌────── hour         (0-23)
│ │ ┌──── day of month (1-31)
│ │ │ ┌── month        (1-12)
│ │ │ │ ┌─ day of week  (0-7, 0 and 7 both mean Sunday)
│ │ │ │ │
* * * * *  command

The mnemonic that helps: the fields go from smallest unit to largest, then sideways into the calendar. Minute, hour, day, month, weekday. Once you've internalized that order, the entire syntax flows from it.

A literal number means "exactly this value." 30 14 * * * means "at 14:30 every day." A * means "every valid value for this field." The combination of literals and stars covers most of what people actually need — the special characters in the next section handle the rest.

The five special characters

* — every value. The default. * * * * * means "every minute of every hour of every day forever," which is almost never what you want but is occasionally useful as a starting point you then constrain.

, — list. 0,15,30,45 * * * * runs at minutes 0, 15, 30, and 45 of every hour. Equivalent here to */15 * * * *; the comma form is preferred when the values are irregular (0,7,33 * * * *).

- — range. 0 9-17 * * 1-5 runs at the top of every hour from 9:00 to 17:00 (inclusive on both ends), Monday through Friday. The range is inclusive — 1-5 means five days, not four.

/ — step. */5 * * * * runs every 5 minutes (at 0, 5, 10, …, 55). The step is taken from the start of the field's range, so */15 in the minute field gives 0, 15, 30, 45 — not 1, 16, 31, 46. Combined with a range: 0-30/5 gives 0, 5, 10, 15, 20, 25, 30.

? — no value (extension, not POSIX). Used in extended cron implementations (Quartz, Spring, AWS, Vixie cron with the ? patch) to disambiguate day-of-month vs day-of-week. POSIX cron doesn't accept it. If your cron parser does, use it; if it doesn't, the equivalent is *.

The day-of-month / day-of-week trap

Standard cron has a behavior most developers don't expect. When both day-of-month (field 3) and day-of-week (field 5) are restricted (i.e., neither is *), the job runs on the OR of the two — not the AND.

Example. 0 9 15 * 1 looks like "at 9:00 AM on the 15th of the month, but only if it's a Monday." It actually means "at 9:00 AM on the 15th of every month, AND at 9:00 AM on every Monday." That's potentially up to 5 fires per month instead of the 0-or-1 you intended.

This is why Quartz/Spring/AWS introduced ? — it lets you say "I'm specifying weekday, ignore day-of-month entirely" or vice versa. In standard POSIX cron, the workaround is to leave one of the fields as * and accept that you can't constrain both directly. If you absolutely need "the 15th, but only if it's a Monday," you handle that in your job script: check the actual date when the job fires, exit early if it's the wrong weekday.

Patterns you'll actually use

The list below covers about 90% of real-world cron expressions. Memorize the shapes; the rest is parameterization.

PatternMeans
0 * * * *Every hour, on the hour
*/5 * * * *Every 5 minutes
0 */6 * * *Every 6 hours (00:00, 06:00, 12:00, 18:00)
0 9 * * *Every day at 9:00 AM
30 2 * * *Every day at 2:30 AM (off-peak — common for backups)
0 9 * * 1-5Every weekday at 9:00 AM
0 9 * * 1Every Monday at 9:00 AM
0 0 1 * *Midnight on the 1st of every month
0 0 1 1 *Midnight on January 1st (yearly)
0 12 * * 1#1Noon on the first Monday of the month (extended cron only)
0 8-18 * * 1-5Top of every hour, 8 AM to 6 PM, weekdays
15,45 * * * *At :15 and :45 past every hour

The named shortcuts

Most modern cron implementations (Vixie cron, the standard on Linux/BSD) accept named shortcuts that expand to a five-field expression:

ShortcutEquivalent
@yearly / @annually0 0 1 1 *
@monthly0 0 1 * *
@weekly0 0 * * 0
@daily / @midnight0 0 * * *
@hourly0 * * * *
@rebootOnce at system startup (special; not equivalent to a cron expression)

The shortcuts are easier to read but harder to grep for in a large crontab. If you're managing more than ~20 cron entries, prefer the explicit five-field form so a search for "daily" doesn't surface unrelated comments.

The four cron mistakes worth knowing about

Forgetting that cron uses the system's local timezone. The cron daemon reads the system timezone (/etc/timezone, the TZ environment variable, or whatever the OS default is) and fires jobs accordingly. A job scheduled 0 9 * * * on a server in Etc/UTC fires at 09:00 UTC, which is 04:00 in New York — usually not what the person who wrote the schedule had in mind. The fix is either (a) explicitly set TZ=America/New_York at the top of the crontab, (b) use a scheduler with explicit timezone fields like Quartz or AWS EventBridge, or (c) accept that all cron times are UTC and document that fact loudly.

The day-of-month / day-of-week OR. See above. The number-one source of "why is this job firing on dates I didn't expect."

Daylight savings transitions. When clocks spring forward, jobs scheduled in the lost hour either don't fire or fire late. When clocks fall back, jobs in the repeated hour fire twice. This affects everything except @hourly-style top-of-hour jobs and is especially nasty for jobs that wake every 30 minutes or every 15 minutes during a DST transition window. The defensive fix is to schedule critical jobs in UTC (which has no DST) and to make jobs idempotent — running twice on a fall-back should be safe, not dangerous.

Overlapping runs. Cron will start your job at the scheduled time even if the previous run hasn't finished. A job scheduled * * * * * that takes 90 seconds to run will pile up — at any given moment you may have 2-3 instances running concurrently, fighting for the same database connection or the same file lock. The fix is a flock-style guard at the start of the job (flock -n /tmp/myjob.lock — myjob.sh) or moving to a scheduler that handles overlap-prevention natively (systemd timers, Kubernetes CronJobs with concurrencyPolicy: Forbid).

Where cron isn't the right tool

Cron is great for "run this script every X." It's poor at:

  • Coordinated multi-step pipelines. If step B depends on step A finishing first, you don't want both as separate cron entries hoping the timing works. Use a workflow runner — Airflow, Prefect, GitHub Actions, even a single shell script that runs A then B.
  • Schedules that depend on data. "Run when the upstream feed has new rows" isn't a cron pattern. That's an event listener, a polling worker, or a queue consumer.
  • Sub-minute schedules. Cron's smallest field is the minute. If you need every-30-seconds, use a daemon, a systemd timer with OnUnitActiveSec=30s, or a long-running loop with sleep 30.
  • One-off future jobs. "At 3 PM next Tuesday." Use at (the BSD/Linux command), or schedule a single job in your application code with a deadline.

Companion tool

If you have a cron expression in front of you and want to know what it actually does: Cron Explainer. Paste any expression, get a plain-English description plus the next 5 fire times in your selected timezone. It validates the syntax before explaining and flags the day-of-month/day-of-week OR trap when both fields are constrained. Standard 5-field POSIX syntax plus the named shortcuts. Everything runs in your browser; the expressions never leave your machine.

Pairs well with the timestamp guide if your cron job is reading or writing dates and you want to make sure the timezone story holds up end-to-end.