A cron expression packs a schedule into five space-separated fields: 30 2 * * 1-5 means 02:30, Monday to Friday. What it leaves out causes most of the trouble: the time zone, whether the scheduler expects a seconds field, and what happens when both day fields are set.
Paste any expression into the cron expression parser, which explains each field and lists the next 10 run times in a time zone you choose. It accepts 5, 6 and 7 fields, the @ shortcuts and L, W and #. The run times below come from its code with "now" fixed at Friday 30 October 2026, 12:00 UTC, and are in UTC unless stated.
The five fields
minute 0–59
hour 0–23
day of month 1–31
month 1–12 or JAN–DEC
day of week 0–6 or SUN–SAT (Vixie cron also takes 7 for Sunday)
| Syntax | Meaning | Example |
|---|---|---|
* | Every value | * in hour: every hour |
, | List | 9,17 in hour: 09:00 and 17:00 |
- | Inclusive range | 1-5 in day of week: Monday to Friday |
/ | Step | */15 in minute: 0, 15, 30, 45 |
Names are case-insensitive. Cron's resolution is one minute; seconds need a dialect.
Seconds, years and other dialects
| System | Fields | Weekday numbers |
|---|---|---|
| Vixie cron, cronie, Kubernetes, GitHub Actions | min hour dom mon dow | 0–6, Sunday = 0 |
| Spring | sec min hour dom mon dow | 0–7, Sunday = 0 or 7 |
| Quartz | sec min hour dom mon dow [year] | 1–7, Sunday = 1 |
| AWS EventBridge | cron(min hour dom mon dow year) | 1–7, Sunday = 1 |
The last column is the dangerous one: 1-5 is Monday to Friday in cron and Spring but Sunday to Thursday in Quartz and AWS. Names mean the same wherever they're accepted, so prefer MON-FRI. The parser says which layout it detected, reads seconds-first weekday numbers the Spring way and warns that Quartz differs. Paste AWS expressions without the cron( ) wrapper.
The extensions: ? L W #
?means no specific value, in the day fields only. Quartz and AWS require it in one of the two; Spring and Kubernetes treat it as*.Lin day of month is the last day (30 November, 28 February). After a weekday it means the last one in the month:5Lis the last Friday in Spring,6Lin Quartz and AWS.Wis the nearest weekday in the same month. 15 November 2026 is a Sunday, so15Wfires on Monday the 16th.LWis the last weekday.#is the nth weekday:MON#1is the first Monday (2 November, 7 December).
L, W and # come from Quartz and also work in Spring and AWS, but not in Vixie cron or cronie, and the Kubernetes and GitHub Actions docs don't list them.
The day-of-month OR day-of-week rule
In Vixie cron and cronie, when both day fields are restricted, a day matches if either field does. The crontab man page's own example, 30 4 1,15 * 5, runs on the 1st and 15th plus every Friday. So 0 0 1 * MON doesn't mean "the 1st, if it's a Monday": it runs on Sunday 1 November, every Monday, and Tuesday 1 December. For the first Monday, restrict one field and test the other in the command (\% is explained under debugging):
0 9 1-7 * * [ "$(date +\%u)" = 1 ] && /usr/local/bin/report.sh
One twist: cronie's source only checks whether a day field starts with *, so 0 0 */2 * MON falls back to AND and means Mondays on odd dates only (9 and 23 November). The parser applies the same rule.
Ten everyday schedules
| Schedule | Expression |
|---|---|
| Every 5 minutes | */5 * * * * |
| Every hour at :00 | 0 * * * * |
| Every day at midnight | 0 0 * * * |
| Weekdays at 09:00 | 0 9 * * 1-5 |
| Every Sunday at midnight | 0 0 * * 0 |
| First day of the month | 0 0 1 * * |
| Last day of the month | Quartz and Spring: 0 0 0 L * ?; crontab: see below |
| Every 15 minutes in business hours | */15 9-17 * * 1-5 |
| Twice a day, 09:00 and 21:00 | 0 9,21 * * * |
| Quarterly, 1 Jan/Apr/Jul/Oct | 0 0 1 */3 * |
For Quartz or Spring, add a seconds field in front and, for Quartz, a ? in the day field you don't use: weekdays at 09:00 becomes 0 0 9 ? * MON-FRI. Note that 9-17 includes the whole 17:00 hour: from Friday 17:20 the parser lists 17:30, 17:45, then Monday 09:00. To stop at 17:00, use 9-16 plus a second line, 0 17 * * 1-5. And quarterly works because steps start at the field's minimum, so */3 in month is 1, 4, 7, 10.
Last day of the month without L
In a crontab, run on every candidate day and let the command check that tomorrow is the 1st:
# GNU date; on macOS and BSD use date -v+1d +\%d
55 23 28-31 * * [ "$(date -d tomorrow +\%d)" = 01 ] && /usr/local/bin/month-end.sh
Or run at 0 0 1 * * and process the previous month.
Why */7 is not every 7 days
A step walks through one field and starts again every period. */7 in day of month is the list 1, 8, 15, 22, 29, so 0 0 */7 * * runs on 29 November and again on 1 December, two days later. Likewise, */35 in minutes fires at :00 and :35; the parser's run list shows 12:35, then 13:00. For weekly, name a day (0 0 * * 1). For a true N-day interval, count days since the epoch:
# every 14 days at 03:00 (whole UTC days since 1970)
0 3 * * * [ $(( $(date +\%s) / 86400 \% 14 )) -eq 0 ] && /usr/local/bin/job.sh
Time zones and daylight saving time
The scheduler, not the expression, sets the time zone:
- Vixie cron and cronie: the daemon's local zone; cronie also accepts a
CRON_TZ=Europe/Berlinline for the whole crontab. - Kubernetes: the kube-controller-manager's zone, unless you set
.spec.timeZone(stable since v1.27).CRON_TZorTZinside the schedule fails validation. - GitHub Actions: UTC, unless you add an IANA
timezonekey next tocron. - AWS EventBridge: scheduled rules use UTC; EventBridge Scheduler accepts a time zone.
- Spring: the server's zone, or the
zoneattribute of@Scheduled.
With the parser set to America/New_York, 0 9 * * * is 13:00 UTC until 31 October and 14:00 UTC from 1 November 2026, when DST ends; a fixed UTC schedule can't follow that shift. To check a run time in another zone or as a Unix timestamp, use the timestamp converter, which shows UTC, local time and one extra zone.
DST also removes one hour a year and repeats another. The parser follows the wall clock literally; in America/New_York:
30 1 * * * Sun 1 Nov 2026 at 01:30 EDT (05:30Z) and again at 01:30 EST (06:30Z)
30 2 * * * Sat 13 Mar 2027, then Mon 15 Mar: no 02:30 on the 14th
cronie and Debian's cron special-case clock changes under three hours for fixed-time jobs: a job in the skipped hour runs right after the jump, and one in the repeated hour runs only once. GitHub Actions with a timezone moves a skipped 02:30 to 03:00, and Quartz's docs warn of skipped or doubled triggers. If a job must run exactly once a day, avoid the small hours or schedule it in UTC.
Shortcuts: @reboot, @daily and friends
| Shortcut | Same as |
|---|---|
@yearly, @annually | 0 0 1 1 * |
@monthly | 0 0 1 * * |
@weekly | 0 0 * * 0 |
@daily, @midnight | 0 0 * * * |
@hourly | 0 * * * * |
@reboot | Once, when the cron daemon starts |
Vixie cron and cronie support all of them. Kubernetes and Spring document all but @reboot (Spring with a seconds field in front), and GitHub Actions supports none.
Debugging a job that never ran
- Did cron try? Look for a
CMDline at the expected minute in/var/log/syslogorjournalctl -u cron(Debian, Ubuntu), or/var/log/cronorjournalctl -u crond(RHEL, Fedora). No line means a wrong schedule or time zone. - Bare environment. cron skips your shell profile and runs jobs under
/bin/shwith a shortPATH. Use absolute paths or setPATH=in the crontab; a temporary* * * * * env > /tmp/cron-env.txtshows what jobs get. - Percent signs. An unescaped
%becomes a newline, and the rest of the line goes to standard input. Write\%. - Lost output. Output is mailed to the owner or
MAILTO; with no mail server, Debian and Ubuntu logNo MTA installed, discarding output, so redirect it to a file. - The file. Scripts need their execute bit (see chmod 755 vs 644). Lines in
/etc/crontaband/etc/cron.d/need a user name before the command, and a crontab's last line needs a newline.
# Broken: the command ends at the first %
0 2 * * * tar czf /backups/site-$(date +%F).tgz /var/www
# Fixed, output kept
0 2 * * * tar czf /backups/site-$(date +\%F).tgz /var/www >> /var/log/backup.log 2>&1
Next steps
Paste the expression from your crontab, CronJob or workflow into the cron parser, set your scheduler's real time zone, and compare the run list with what you expect. If cron fires on time but your logs or API disagree, read debugging timezone errors in APIs.