basics standard reference

How Cron Expressions Work: A Complete Field Reference

by Sinthuyan Arulselvam  · April 1, 2026

Cron expressions are the lingua franca of scheduled automation. Whether you are draining a queue at midnight, pruning logs every Sunday, or polling an API every five minutes, a cron expression is how you declare that intent in a single compact string. This reference covers everything from the basics of field semantics to the quirks of extended dialects used by Quartz, AWS EventBridge, and Jenkins — so you can write schedules with confidence and debug misfires without guesswork.

What Is a Cron Expression?

A cron expression is a whitespace-delimited string of fields that encodes a recurring point in time. The classic Unix format contains five fields; extended dialects may include a seconds field, a year field, or both. The cron daemon — or scheduler library — evaluates the current time against the expression once per minute, at the top of each minute. If, and only if, every field matches the current calendar value simultaneously, the job fires.

That "simultaneously" clause matters more than most documentation lets on. A cron expression does not describe a duration or an interval. It describes a set of moments. The string */15 9-17 * * 1-5 does not mean "every 15 minutes for 8 hours on weekdays." It means "the moments in time where the minute value is 0, 15, 30, or 45 and the hour value is between 9 and 17 and the day-of-week is Monday through Friday." The distinction matters when you are reasoning about gaps, overlaps, and missed firings.

The Five Fields

Standard cron uses five positional fields separated by spaces. Their order is fixed and their meaning is determined by position, not by name.

Position Field Allowed Range Notes
1 Minute 0–59 Evaluated first; most granular unit in standard cron
2 Hour 0–23 24-hour clock; 0 is midnight
3 Day of Month 1–31 Note: starts at 1, not 0
4 Month 1–12 Some dialects accept JAN–DEC abbreviations
5 Day of Week 0–7 0 and 7 both represent Sunday in most Unix implementations; SUN–SAT abbreviations common

Minute (Field 1)

The minute field controls the sub-hour granularity of your schedule. A value of 30 fires once per hour, at half past. A step expression like */10 fires at minutes 0, 10, 20, 30, 40, and 50 — six times per hour. You cannot schedule anything more granular than one minute in standard cron; for sub-minute scheduling you need a different mechanism entirely.

Hour (Field 2)

Hours run from 0 (midnight) to 23 (11 PM). A value of 0 combined with 0 in the minute field gives you exactly midnight: 0 0 * * *. Ranges like 9-17 are common for business-hours schedules. Remember that the timezone the daemon uses is the system timezone of the host, unless overridden — an easy source of off-by-one-hour bugs around DST transitions.

Day of Month (Field 3)

Day-of-month runs from 1 to 31. The scheduler accounts for months with fewer days — specifying day 31 in a month that only has 30 days simply skips that occurrence. The interaction between day-of-month and day-of-week is one of the most commonly misunderstood aspects of cron and is covered in detail in the evaluation section below.

Month (Field 4)

Months run from 1 (January) to 12 (December). Most implementations also accept three-letter abbreviations: JAN, FEB, MAR, and so on. A value of */3 fires in months 1, 4, 7, and 10 — quarterly, starting in January. A list like 3,6,9,12 fires at the end of each quarter.

Day of Week (Field 5)

Day-of-week is where cron dialects diverge most visibly. In standard Unix cron, 0 and 7 are both valid representations of Sunday, giving you a range of 0–7. Most implementations also accept abbreviations SUN through SAT. Monday is 1 in all standard implementations, though Quartz-based schedulers number Monday as 2 (with Sunday as 1). Always check your dialect's documentation when porting an expression between systems.

Special Characters

Four special characters modify how individual fields are interpreted. They are supported universally across all standard and extended cron dialects.

Wildcard: *

The asterisk matches every valid value for that field. In the minute position, * matches 0 through 59. In the month position, it matches 1 through 12. A fully wildcarded expression * * * * * fires every minute of every day, all year.

bash
# Every minute
* * * * *

# Every minute of every Monday
* * * * 1

# Every minute between 9 AM and 5 PM
* 9-17 * * *

List: ,

A comma separates discrete values within a single field. Lists can contain bare integers, ranges, or step expressions mixed together. The scheduler treats each element independently and fires whenever any element matches.

bash
# At 8 AM and 6 PM every day
0 8,18 * * *

# Monday, Wednesday, Friday at noon
0 12 * * 1,3,5

# Minutes 0, 15, 30, 45 every hour
0,15,30,45 * * * *

Range: -

A hyphen defines an inclusive range between two values. The lower bound must come first. Ranges can be combined with lists and step expressions in the same field.

bash
# Every hour from 9 AM through 5 PM
0 9-17 * * *

# Weekdays only (Monday through Friday)
0 9 * * 1-5

# January through March
0 0 1 1-3 *

Step: /

The forward slash defines a step value that selects every Nth value from a range or wildcard. The syntax is range/step or */step. The starting point of the range, not the current time, determines which values are selected. This is a common source of confusion and is addressed directly in the misconceptions section.

bash
# Every 5 minutes (at 0, 5, 10, 15 ... 55)
*/5 * * * *

# Every 2 hours starting from midnight (0, 2, 4 ... 22)
0 */2 * * *

# Every 10 minutes from 9 AM to 5 PM on weekdays
*/10 9-17 * * 1-5

# Every 6 hours (0, 6, 12, 18)
0 */6 * * *

Extended Characters

The following characters are not part of the POSIX/Unix standard. They are supported in extended dialects such as Quartz (Java), AWS EventBridge, and Spring. Using them in a standard Unix crontab will produce a parse error.

Question Mark: ?

Supported in: Quartz, EventBridge, Spring. The question mark means "no specific value" and is used in the day-of-month and day-of-week fields to avoid the ambiguity of specifying both. In Quartz, you must use ? in exactly one of those two fields whenever the other has a non-wildcard value. For example, to fire on the 15th of every month regardless of what day of the week it falls on, you write 0 0 15 * ?.

Last: L

Supported in: Quartz, EventBridge. In the day-of-month position, L means the last day of the month — the scheduler resolves this dynamically per month (28, 29, 30, or 31). In the day-of-week position, L appended to a day number means the last occurrence of that weekday in the month. For example, 5L in the day-of-week field means the last Friday of the month.

bash
# Last day of every month at midnight (Quartz)
0 0 0 L * ?

# Last Friday of every month at 6 PM (Quartz)
0 0 18 ? * 6L

Weekday: W

Supported in: Quartz, EventBridge. The W character in the day-of-month field selects the nearest weekday (Monday–Friday) to the given date. If the 15th is a Saturday, the scheduler fires on Friday the 14th. If the 15th is a Sunday, it fires on Monday the 16th. The LW combination — last weekday of the month — is also valid in Quartz.

Nth Occurrence: #

Supported in: Quartz. The hash character in the day-of-week field selects the Nth occurrence of a given weekday within the month. The syntax is weekday#N. For example, 2#1 means the first Monday of the month, and 6#3 means the third Friday of the month. This is far more reliable than trying to approximate "first Monday" using DOM ranges.

bash
# First Monday of every month at 9 AM (Quartz)
0 0 9 ? * 2#1

# Third Friday of every month at noon (Quartz)
0 0 12 ? * 6#3

Practical Examples

The following patterns cover the scenarios you will encounter in the vast majority of real-world scheduling work. All expressions use standard five-field format unless noted.

  • Daily at midnight: 0 0 * * * — fires once per day at 00:00.
  • Every hour on the hour: 0 * * * * — fires 24 times per day.
  • Every 5 minutes: */5 * * * * — fires at 0, 5, 10 ... 55 past each hour.
  • Every 15 minutes during business hours on weekdays: */15 9-17 * * 1-5 — fires at quarter-hour marks from 9 AM to 5 PM, Monday through Friday.
  • Weekly on Sunday at 2 AM: 0 2 * * 0 — a common slot for maintenance windows and database backups.
  • First day of every month at 6 AM: 0 6 1 * * — useful for monthly billing runs or report generation.
  • Weekdays only at 8:30 AM: 30 8 * * 1-5 — fires Monday through Friday at half past eight.
  • Twice daily at 6 AM and 6 PM: 0 6,18 * * * — fires exactly twice per day using a list in the hour field.
  • Every 30 minutes between midnight and 6 AM: */30 0-6 * * * — fires at 0:00, 0:30, 1:00, 1:30 ... 6:00, 6:30 (note: 6:30 is included because the range is evaluated inclusive of the upper bound before the step filter).
  • Quarterly on the first of the month at noon: 0 12 1 */3 * — fires January 1st, April 1st, July 1st, and October 1st.
  • Every weekday at 9 AM except holidays — cron cannot model exceptions natively; implement this by wrapping the job command in a script that checks a holiday calendar.
  • Every 20 minutes starting from the top of the hour: 0,20,40 * * * * — explicitly listing values is clearer than */20 when exact positions matter to you.

How the Scheduler Evaluates an Expression

Understanding the evaluation model prevents a large class of "my job didn't fire" bugs. Here is what happens at the top of every minute:

  1. The daemon reads the current wall-clock time, truncated to the current minute.
  2. It evaluates each field in the expression independently against the corresponding time component.
  3. If all five fields match, the job is dispatched. If any field does not match, the job is skipped for this minute.
  4. The daemon sleeps until the top of the next minute and repeats.

The critical subtlety is how day-of-month and day-of-week interact. In standard Unix cron (as specified by POSIX and implemented in Vixie cron), if both day-of-month and day-of-week are non-wildcard values, the scheduler uses OR logic: it fires if the current day matches either the day-of-month or the day-of-week. This is counterintuitive — most people expect AND logic. The expression 0 0 1 * 5 fires on the first day of every month and on every Friday, not only on Fridays that happen to fall on the 1st. To get AND logic (first day of month only if it is also a Friday), you need to implement that check inside the job script itself, or use a dialect like Quartz that provides the ? operator to disambiguate.

Common Misconceptions

Step values do not track elapsed time

*/5 in the minute field does not mean "every 5 minutes from when the daemon started" or "5 minutes after the last run." It means "at minute values that are divisible by 5." The starting point is always the bottom of the valid range for that field — 0 for minutes, 0 for hours, 1 for days. If your daemon starts at 12:03, the first */5 firing will be at 12:05, not 12:08.

Cron is not an interval timer

If a job takes longer to complete than the interval between firings, cron will fire a second instance while the first is still running — unless your cron daemon or job wrapper has overlap protection. */1 * * * * fired at 12:00 does not guarantee the next firing waits until the job finishes; it fires again at 12:01 regardless. Use a lock file, a mutex, or a purpose-built job queue if you need exclusive execution.

Sunday is 0 or 7, depending on the implementation

Both 0 and 7 represent Sunday in most Unix cron implementations. However, some tools (including certain versions of cronie and fcron) treat 7 as invalid. Quartz numbers Sunday as 1 and Saturday as 7 — a completely different scheme. Always verify the numbering convention for the scheduler you are targeting before deploying a day-of-week expression.

Field count varies by dialect

Copy-pasting a cron expression between systems without checking the dialect is a common source of silent misconfiguration. A six-field Quartz expression pasted into a standard Unix crontab will be parsed incorrectly, with the first field misinterpreted as the minute rather than seconds. Always count the fields and check the target dialect before migrating a schedule.

Missed firings during DST transitions

When clocks spring forward, the hour between 2:00 and 3:00 AM effectively does not exist. Any job scheduled to fire during that window will be skipped. When clocks fall back, the same hour occurs twice, and depending on the implementation, your job may fire once or twice. This is rarely handled automatically by the scheduler — design your jobs to be idempotent so that running twice produces the same outcome as running once.

Standard vs. Extended Dialects

The five-field standard is the baseline, but most production environments involve at least one extended dialect. The table below summarises the key differences.

Dialect Fields Field Order Extended Chars Notes
Unix / POSIX (Vixie cron) 5 min hour DOM month DOW None The reference implementation; man 5 crontab
Quartz (Java) 6–7 sec min hour DOM month DOW [year] ? L W # DOW uses 1=Sun through 7=Sat numbering; ? required in DOM or DOW
AWS EventBridge 6 min hour DOM month DOW year ? L W # Year field is mandatory; uses ? in DOM/DOW like Quartz
Spring @Scheduled 6 sec min hour DOM month DOW ? L W # Follows Quartz conventions; seconds field prepended
Jenkins (H syntax) 5 min hour DOM month DOW H H hashes the job name to distribute load; H/15 = every 15 minutes at a consistent but arbitrary offset
GitHub Actions 5 min hour DOM month DOW None Standard syntax; minimum interval is every 5 minutes; runs may be delayed under load

The Jenkins H Symbol

Jenkins introduces the H (hash) symbol to solve a real operational problem: when hundreds of jobs are all scheduled with 0 * * * *, every one of them fires at exactly the top of the hour, creating a thundering herd that spikes CPU and I/O. The H symbol tells Jenkins to derive a pseudo-random but deterministic minute offset from the job name. H * * * * means "once per hour, at a minute derived from this job's name." H/15 * * * * means "every 15 minutes, starting at that same derived offset." The result is automatic load distribution without manual coordination. No other major scheduler has adopted this convention, so treat it as Jenkins-specific.

Writing Defensively

A few habits that will save you debugging time in production:

  • Always validate expressions before deploying. Use a tool like CronBase.dev to confirm the human-readable description matches your intent before the expression goes anywhere near a production system.
  • Prefer explicit lists over steps when precision matters. 0,30 * * * * is unambiguous. */30 * * * * is equivalent, but a reader has to mentally evaluate the step to confirm it. For critical schedules, explicit is better.
  • Comment your crontab entries. # Nightly DB vacuum — must complete before 4 AM backup window above a complex expression is worth far more than the five seconds it takes to write.
  • Test around DST transition dates. If your service crosses a timezone boundary twice a year, verify that jobs scheduled in the 1–3 AM window behave as expected in both the spring-forward and fall-back directions.
  • Check your timezone. System cron runs in the server's local timezone. For containerised workloads or multi-region deployments, set the TZ variable explicitly or use UTC throughout to avoid drift.

Quick Reference

The anatomy of a standard five-field expression, in plain English:

bash
  ${minute} ${hour} ${day-of-month} ${month} ${day-of-week}
      |          |            |              |           |
   0-59       0-23          1-31          1-12         0-7
                                                   (0 and 7 = Sunday)

And the special characters at a glance: * (any), , (list), - (range), / (step). Extended: ? (unspecified, Quartz/EventBridge), L (last, Quartz/EventBridge), W (nearest weekday, Quartz/EventBridge), # (Nth weekday, Quartz), H (hash, Jenkins).

Cron expressions reward careful reading. The five-field format packs a surprising amount of scheduling power into a very small string — once you internalise the field order, the special characters, and the OR-semantics of DOM+DOW, you will be able to both write and audit expressions fluently. The dialect differences are real and consequential, but they follow a consistent pattern: extended dialects prepend a seconds field, append a year field, or add operators to resolve DOM/DOW ambiguity. Know which dialect your scheduler implements, and the rest is just arithmetic.

Explore related resources

Related seed schedules

Related Guides