Cron Expression Syntax Explained
Cron expressions are one of those topics developers learn once, forget the details, and then look up again every six months. A five-character string like 30 9 * * 1-5 can encode a surprisingly specific schedule — "every weekday at 9:30 AM" — but the syntax has just enough edge cases to cause real confusion: 0-indexed vs 1-indexed fields, Sunday being both 0 and 7, the difference between the Unix standard and Quartz's six-field extension, and the subtle behavior of step values combined with ranges.
This guide covers cron syntax from first principles, then works through every special character with concrete examples. Whether you are writing a cron job for a Linux daemon, configuring a GitHub Actions schedule, or setting up an AWS EventBridge rule, the same underlying concepts apply — with a few important platform differences explained along the way.
The Five Standard Fields
A standard cron expression has five space-separated fields, read left to right: minute, hour, day of month, month, and day of week. Each field constrains when the job runs along that dimension. A job runs only when all five fields match simultaneously.
# Field Position Range Special chars
# ─────────────────────────────────────────────────
# Minute 1 0-59 * / , -
# Hour 2 0-23 * / , -
# Day of month 3 1-31 * / , - ? L W
# Month 4 1-12 or JAN-DEC * / , -
# Day of week 5 0-7 (0=7=Sun) or SUN-SAT * / , - ? L #
Reading an expression like 30 9 * * 1-5: minute 30, hour 9, any day of month, any month, day-of-week 1 through 5 (Monday through Friday). Result: 9:30 AM every weekday.
Field 1: Minute (0–59)
The minute field specifies which minute(s) of the hour the job should run. 0 means the top of the hour, 30 means half past, 59 means one minute before the hour turns. Using * runs the job every minute.
Field 2: Hour (0–23)
Hours use 24-hour time. 0 is midnight, 12 is noon, 23 is 11:00 PM. There is no AM/PM in cron — a job at 9:00 AM is hour 9, and a job at 9:00 PM is hour 21.
Field 3: Day of Month (1–31)
Unlike the other fields, day-of-month starts at 1, not 0. Valid values are 1 through 31. If you specify a day that does not exist in a given month (for example, day 31 in April), the job simply does not run that month. In Quartz, the special character L represents the last day of the month, regardless of how many days are in it.
Field 4: Month (1–12)
Month values run from 1 (January) through 12 (December). Many schedulers also accept three-letter abbreviations: JAN, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV, DEC. These are case-insensitive in most implementations.
Field 5: Day of Week (0–7)
This is the field with the most historical baggage. Both 0 and 7 represent Sunday — this is a compatibility quirk from early Unix. Monday is 1, Tuesday 2, through Saturday 6. Three-letter names work here too: SUN, MON, TUE, WED, THU, FRI, SAT.
When both day-of-month and day-of-week are specified (neither is *), most Unix cron daemons use OR logic: the job runs if it matches either field. This surprises many developers. If you want "the 15th, but only if it is a weekday," you need to handle that in the job script itself, not in the cron expression.
Special Characters
Asterisk (*) — Every Value
The asterisk matches every valid value for a field. In the minute field, * means minutes 0 through 59. In the month field, it means all 12 months. It is the most common character in cron expressions.
Slash (/) — Step Values
The slash defines a step interval. */5 in the minute field means "every 5 minutes." More precisely, it means "every value in the range that is divisible by 5 starting from the first value in the range." So */5 in minutes evaluates to 0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55.
Steps can be combined with ranges: 10-30/5 in the minute field means every 5 minutes between minute 10 and minute 30, yielding 10, 15, 20, 25, 30. The step starts from the range's lower bound.
Comma (,) — List of Values
Commas create a list of specific values. 1,3,5 in the month field means January, March, and May. 9,17 in the hour field means 9:00 and 17:00. You can list as many values as needed and combine them with other constructs.
Dash (-) — Range
A dash specifies an inclusive range. 1-5 in the day-of-week field means Monday through Friday. 9-17 in the hour field means 9 AM through 5 PM. Ranges are inclusive on both ends.
Question Mark (?) — No Specific Value (Quartz Only)
Standard Unix cron does not support ?. In Quartz Scheduler, it is used in the day-of-month or day-of-week fields to mean "I do not care about this field." Because Quartz cannot always resolve conflicts between day-of-month and day-of-week, one of the two must be set to ? when the other is specified. 0 0 15 * ? means "the 15th of every month, any day of week." 0 0 ? * MON means "every Monday, any day of month."
L — Last (Quartz Only)
L in the day-of-month field means the last day of the month. L in the day-of-week field means Saturday, or when combined with a number (e.g., 5L), the last occurrence of that weekday in the month. 5L is the last Friday of the month.
W — Nearest Weekday (Quartz Only)
W in the day-of-month field finds the nearest weekday (Monday–Friday) to the given day. 15W means the nearest weekday to the 15th. If the 15th is a Saturday, the job runs on the 14th (Friday). If it is a Sunday, it runs on the 16th (Monday). W does not cross month boundaries.
Hash (#) — Nth Weekday of Month (Quartz Only)
# specifies the Nth occurrence of a weekday in a month. 5#2 means the second Friday of the month. 2#1 means the first Tuesday. If the specified occurrence does not exist in a given month, the job does not run that month.
Common Cron Patterns
# Every minute
* * * * *
# Every day at midnight (00:00)
0 0 * * *
# Every day at 9:30 AM
30 9 * * *
# Every Monday at 8:00 AM
0 8 * * 1
# First day of every month at noon
0 12 1 * *
# Every 15 minutes
*/15 * * * *
# Weekdays at 6:00 PM
0 18 * * 1-5 You can test and validate any of these using the Cron Parser tool, which shows the next five run times for any expression and highlights invalid syntax. Use the Timestamp Converter if you need to verify what a specific Unix timestamp corresponds to in your local timezone.
The 6-Field Extension: Quartz Scheduler
Quartz Scheduler — used heavily in the Java ecosystem and adopted by many enterprise schedulers — adds a mandatory seconds field at the beginning of the expression. The format becomes: second minute hour day-of-month month day-of-week.
# Quartz 6-field: second minute hour day month weekday
# Run at exactly 00 seconds, 30 minutes, every hour
0 30 * * * ?
# Run at midnight every day
0 0 0 * * ?
# Run every 5 seconds
0/5 * * * * ?
# Last day of month at 10:00 AM
0 0 10 L * ?
# Last Friday of month at 3:00 PM
0 0 15 ? * 6L
Key differences from standard cron: the seconds field (0–59) is first and required; day-of-week uses 1 (Sunday) through 7 (Saturday), not 0–6; ? is required in either day-of-month or day-of-week when the other is specified; and L, W, and # are valid special characters.
Never paste a Quartz expression into a Unix crontab without removing the seconds field and adjusting the day-of-week numbering. The parsers are not interchangeable.
Cron in Different Systems
Linux/Unix crontab (Vixie cron)
The original and most common form. Five fields, space-separated, followed by the command. Environment variables like CRON_TZ or TZ set the timezone. User crontabs are edited with crontab -e; system crontabs live in /etc/cron.d/. The @reboot, @daily, @hourly, @weekly, and @monthly shortcuts are widely supported as aliases for common patterns.
GitHub Actions
GitHub Actions uses standard 5-field cron syntax, but the timezone is always UTC — there is no way to specify a local timezone in the expression itself. Expressions are evaluated at the repository level, not per job. Note that scheduled workflows can be delayed by several minutes during high-load periods on GitHub's infrastructure.
# GitHub Actions schedule syntax (5-field UTC only)
on:
schedule:
# Run every day at 02:30 UTC
- cron: '30 2 * * *'
# Run every Monday at 09:00 UTC
- cron: '0 9 * * 1' The official GitHub Actions cron documentation notes that the minimum interval is 5 minutes — expressions that would run more frequently than that are silently ignored.
AWS EventBridge (CloudWatch Events)
AWS EventBridge supports both rate expressions (rate(5 minutes)) and cron expressions. Their cron variant is 6-field but differs from Quartz: day-of-week uses Sun–Sat names or 1–7 where 1 is Sunday, all times are UTC, and at least one of day-of-month or day-of-week must be ?. AWS does not support the W or # characters.
# AWS EventBridge (cron in UTC, 6-field variant)
# Runs every day at 10:00 AM UTC
cron(0 10 * * ? *)
# Runs at 6:00 PM on the last weekday of every month
cron(0 18 L-1 * ? *) Kubernetes CronJob
Kubernetes CronJob uses standard 5-field cron syntax. The timezone defaults to the timezone of the kube-controller-manager process (usually UTC on managed clusters). Kubernetes 1.25 added the timeZone field to the CronJob spec, allowing per-job timezone configuration without relying on system timezone.
Node.js (node-cron / cron npm)
The popular node-cron package supports standard 5-field expressions plus an optional 6th seconds field prepended. The cron package is Quartz-compatible. Both support timezone strings (e.g., America/New_York) as a constructor option. Check your specific package's documentation to confirm which format it expects.
Common Pitfalls
Timezone assumptions
Cron runs in the server or container timezone, which is frequently UTC in cloud environments. A job intended to run at "9 AM business time" set as 0 9 * * * will run at 9 AM UTC — which could be 4 AM, 5 AM, or 11 AM in your local timezone depending on offset and DST. Always set CRON_TZ, use the scheduler's timezone field, or convert your target time to UTC explicitly. The Timezone Converter can help you find the UTC equivalent for any local time across any timezone.
Day-of-month / day-of-week OR logic
In Unix cron, when both day-of-month and day-of-week fields are non-*, the daemon runs the job if either matches. So 0 9 15 * 1 runs on the 15th of every month AND every Monday — not only on Mondays that happen to be the 15th. To achieve AND logic, you must implement the additional check inside the job script.
Confusing step values with ranges
*/5 means "every 5 minutes starting from 0" (0, 5, 10 ...). 5/5 means "every 5 minutes starting from 5" (5, 10, 15 ...). 5-30/5 means "every 5 minutes between minute 5 and minute 30" (5, 10, 15, 20, 25, 30). Mixing these with ranges can produce unexpected results if you are not deliberate about the start value.
The "every second" trap
Cron's minimum resolution in the standard 5-field format is one minute. You cannot schedule a cron job to run every second or every few seconds using a crontab. For sub-minute scheduling, use a language-level timer (setInterval in Node.js, APScheduler in Python) or a purpose-built queue system rather than cron. If you need every 30 seconds, run two cron jobs: one at * * * * * and one that sleeps 30 seconds before executing.
Missing the month numbering
In standard cron, months run from 1 (January) to 12 (December). In some programming language date APIs, months are 0-indexed (0 = January, 11 = December). If you are generating cron expressions programmatically from date objects, double-check that you are not off by one in the month field.
Validate Your Expressions
The Cron Parser on Toova shows the next five scheduled run times for any expression, supports both 5-field and 6-field Quartz syntax, and flags invalid values inline. For quick sanity-checking, crontab.guru is a well-known interactive tool that describes expressions in plain English. Both are useful to keep bookmarked.
When working with timestamps and scheduled times, the Timestamp Converter translates between Unix timestamps and human-readable dates across any timezone. For jobs that involve timezone-sensitive scheduling, pair it with the Timezone Converter to verify offsets, DST transitions, and UTC equivalents before writing your final expression.