Cron Expressions Explained: Syntax, Examples, and Platform Differences
A cron expression is five fields separated by spaces (minute, hour, day of month, month, day of week) that tell a scheduler when to run a job. For example, 30 2 * * * runs a job at 2:30 AM every day, and */15 9-17 * * 1-5 runs it every 15 minutes from 9:00 AM through 5:45 PM on weekdays. An asterisk matches every value, a comma lists values, a hyphen makes a range and a slash sets a step.
This guide covers the standard five-field format, the extra characters used by Quartz, Spring and AWS, 15 expressions you can copy, how Linux, AWS EventBridge, GitHub Actions and Kubernetes differ, and the mistakes behind most failed jobs. To check any expression, paste it into our Cron Expression Parser, which shows a plain-English description and the next 10 run times in your browser's time zone.
The 5-Field Cron Format
A standard cron expression has five fields separated by spaces. This is the format read by the Linux crontab, GitHub Actions and Kubernetes:
โโโโโโโโโโโโโโ minute (0 to 59)
โ โโโโโโโโโโโโโโ hour (0 to 23)
โ โ โโโโโโโโโโโโโโ day of month (1 to 31)
โ โ โ โโโโโโโโโโโโโโ month (1 to 12, or JAN to DEC)
โ โ โ โ โโโโโโโโโโโโโโ day of week (0 to 7, or SUN to SAT; 0 and 7 are Sunday)
โ โ โ โ โ
* * * * *
Each field takes a single value, a range, a list or a step. The job runs when the current time matches all five fields, with one exception for the two day fields that is covered under common mistakes below.
| Field | Allowed values | Notes |
|---|---|---|
| Minute | 0 to 59 | |
| Hour | 0 to 23 | 24-hour clock, so 0 is midnight and 13 is 1 PM |
| Day of month | 1 to 31 | A job set for the 31st skips months with fewer days |
| Month | 1 to 12 or JAN to DEC | Names are not case sensitive |
| Day of week | 0 to 7 or SUN to SAT | 0 and 7 both mean Sunday |
POSIX, the formal standard, is narrower than what Linux cron accepts. It defines only numbers, asterisks, lists and ranges, and its day of week runs from 0 to 6. Steps such as */5, month and day names, 7 for Sunday and the @daily style macros are extensions. Vixie-style crons such as cronie and Debian's cron support all of them, so this guide treats them as standard.
Special Characters Explained
| Character | Name | Meaning | Example |
|---|---|---|---|
* | Wildcard | Every value in the field | * * * * * runs every minute |
, | List | Several specific values | 0 8,12,18 * * * runs at 8 AM, noon and 6 PM |
- | Range | Every value from one to another, inclusive | 0 9-17 * * * runs every hour from 9 AM to 5 PM |
/ | Step | Every nth value of a range or of * | */15 * * * * runs every 15 minutes; 0-30/10 * * * * runs at minutes 0, 10, 20 and 30 |
Extra characters in Quartz, Spring and AWS
Java schedulers and AWS add characters that Linux cron does not understand. Quartz and Spring also put a seconds field first, so the examples below have six fields. Quartz and AWS require a ? in one of the two day fields, and Spring allows it. AWS EventBridge has no seconds field and ends with a year field instead.
| Character | Meaning | Example (seconds first) | Result |
|---|---|---|---|
? | No specific value, used in day of month or day of week | 0 0 12 ? * MON | Noon every Monday |
L | The last day of the month, or the last given weekday | 0 0 0 L * ? | Midnight on the last day of every month |
W | The weekday nearest a given day of the month | 0 0 0 15W * ? | Midnight on the weekday nearest the 15th |
# | The nth given weekday of the month | 0 0 0 ? * FRI#3 | Midnight on the third Friday of every month |
These characters are not supported in Linux crontab, GitHub Actions or Kubernetes. Day-of-week numbers also differ: Quartz and AWS count from 1 (Sunday) to 7, so 5#3 there means the third Thursday. Day names such as FRI mean the same thing everywhere they are supported, so prefer them.
15 Common Cron Expressions (With Plain English)
| # | Expression | Plain English |
|---|---|---|
| 1 | * * * * * | Every minute |
| 2 | */5 * * * * | Every 5 minutes |
| 3 | 0 * * * * | Every hour, at minute 0 |
| 4 | 0 0 * * * | Every day at midnight |
| 5 | 0 9 * * * | Every day at 9:00 AM |
| 6 | 0 9 * * 1-5 | Every weekday at 9:00 AM |
| 7 | 0 0 * * 0 | Every Sunday at midnight |
| 8 | 0 0 1 * * | The first day of every month at midnight |
| 9 | 0 0 1 1 * | January 1 at midnight (yearly) |
| 10 | 30 4 * * * | Every day at 4:30 AM |
| 11 | 0 */2 * * * | Every 2 hours, on the hour (12 AM, 2 AM and so on) |
| 12 | 0 9,18 * * * | Twice a day, at 9:00 AM and 6:00 PM |
| 13 | */30 9-17 * * 1-5 | Every 30 minutes on weekdays, from 9:00 AM through 5:30 PM |
| 14 | 0 0 1,15 * * | The 1st and 15th of every month at midnight |
| 15 | 0 3 * * 6 | Every Saturday at 3:00 AM |
The last run of */30 9-17 * * 1-5 is 5:30 PM, because the hour range includes 17 and the step fires at minutes 0 and 30 of every hour in it. To stop at 5:00 PM, use */30 9-16 * * 1-5 plus a separate 0 17 * * 1-5. Paste any of these into the Cron Expression Parser to see the next 10 run times. It reads the standard five-field format, including names and steps, but not @ macros or the Quartz characters above.
Cron Across Different Systems
The core syntax is shared, but each platform has its own extras and its own default time zone.
Linux crontab
Edit your own table with crontab -e. Each line is the five time fields followed by the command:
# Run the backup script every day at 2:30 AM
30 2 * * * /home/user/scripts/backup.sh
# Delete export files older than 7 days, every Sunday at midnight
0 0 * * 0 find /home/user/exports -type f -mtime +7 -delete
# Health check every 5 minutes; -f makes curl fail on HTTP errors
*/5 * * * * curl -fsS https://example.com/health >> "$HOME/health.log" 2>&1
Files in /etc/cron.d/ and /etc/crontab add a sixth field before the command that names the user to run as. Vixie-style crons also accept these shorthand macros, which are not part of POSIX:
| Shorthand | Equivalent |
|---|---|
@reboot | Run once after the system starts |
@yearly or @annually | 0 0 1 1 * |
@monthly | 0 0 1 * * |
@weekly | 0 0 * * 0 |
@daily or @midnight | 0 0 * * * |
@hourly | 0 * * * * |
GitHub Actions does not support these macros. Kubernetes CronJobs accept all of them except @reboot.
AWS EventBridge
AWS uses six fields: minutes, hours, day of month, month, day of week and year. The expression sits inside cron(), days of week are numbered 1 (Sunday) to 7, and one of the two day fields must be ? because you cannot give a value or * to both. EventBridge scheduled rules always run in UTC. AWS recommends the newer EventBridge Scheduler for new work, and it lets you pick a time zone. Both also accept rate() expressions.
# Minutes Hours Day-of-month Month Day-of-week Year
cron(0 9 ? * MON-FRI *) # Weekdays at 9:00 AM UTC
cron(0/15 * * * ? *) # Every 15 minutes
cron(0 0 1 * ? *) # First of every month at midnight UTC
# Rate expressions
rate(5 minutes)
rate(1 hour)
rate(7 days)
GitHub Actions
GitHub Actions reads five-field POSIX-style cron in the schedule trigger. Day of week runs from 0 to 6 (or SUN to SAT), and @ macros are not supported. Times are UTC unless you add a timezone key with an IANA name such as America/New_York, an option GitHub added in March 2026.
on:
schedule:
- cron: '0 9 * * 1-5' # Weekdays at 9:00 AM UTC
- cron: '30 5 * * 1-5' # Weekdays at 5:30 AM New York time
timezone: 'America/New_York'
The shortest interval is once every 5 minutes. Scheduled workflows run only on the default branch, and in a public repository GitHub disables them after 60 days without repository activity. Runs can start late when GitHub is busy, especially at the top of the hour, so avoid minute 0 for jobs that are not time critical.
Kubernetes CronJob
A Kubernetes CronJob takes a standard five-field schedule. Without a timeZone, the schedule follows the local time zone of the kube-controller-manager. Setting .spec.timeZone to an IANA name fixes that, and the field has been stable since Kubernetes v1.27. Kubernetes rejects CRON_TZ or TZ written inside the schedule string. Validate your manifest with our YAML Formatter before you apply it.
apiVersion: batch/v1
kind: CronJob
metadata:
name: db-backup
spec:
schedule: "30 2 * * *" # Daily at 2:30 AM in the time zone below
timeZone: "America/New_York" # Stable since Kubernetes v1.27
concurrencyPolicy: Forbid # Skip a run if the previous one is still active
jobTemplate:
spec:
template:
spec:
containers:
- name: backup
image: postgres:18
# Prints the dump to the job log; write it to a volume or bucket in real use
command: ["pg_dump", "-h", "db", "-U", "app", "mydb"]
restartPolicy: OnFailure
Quartz and Spring (Java)
Quartz and Spring put seconds first. Quartz accepts an optional year as a seventh field, while Spring always has six. Spring numbers days of week 0 to 7 (0 and 7 are Sunday) and Quartz numbers them 1 to 7 (1 is Sunday). 0 0 9 ? * MON-FRI means 9:00:00 AM on weekdays in both.
Common Pitfalls and How to Avoid Them
1. Time zone confusion
Check which time zone your scheduler uses before you write the hour field. Linux cron uses the system's local time zone. Cronie (the cron on Fedora, RHEL and related distributions) also honors a CRON_TZ line in the crontab, but Debian's and Ubuntu's cron does not support per-crontab time zones and ignores it. EventBridge scheduled rules always use UTC, while EventBridge Scheduler lets you choose a zone. GitHub Actions uses UTC unless you set timezone. A Kubernetes CronJob follows the kube-controller-manager unless you set .spec.timeZone.
Fix: do not hard-code a UTC offset for a local-time job, because it is wrong for half the year. 9 AM Eastern is 14:00 UTC in winter (UTC-5) but 13:00 UTC in summer (UTC-4), so 0 14 * * * fires at 10 AM local time all summer. Set a time zone instead where your scheduler allows it, and use the Time Zone Converter to see what a local time is in UTC on a given date:
# cronie (Fedora, RHEL): set the time zone at the top of the file
CRON_TZ=America/New_York
0 9 * * * /scripts/morning-report.sh
On Debian and Ubuntu, set the server's time zone, or use a systemd timer, whose OnCalendar accepts a zone (for example OnCalendar=Mon..Fri 09:00 America/New_York).
2. Overlapping runs
If a job takes 10 minutes but is scheduled every 5 minutes, runs pile up. That can corrupt data, exhaust memory or deadlock a database.
Fix: take a lock so a new run exits when the last one is still going:
# Linux: flock -n fails immediately instead of waiting for the lock
*/5 * * * * /usr/bin/flock -n /tmp/myjob.lock /scripts/long-running-job.sh
# Kubernetes: skip the new run while the previous Job is active
spec:
concurrencyPolicy: Forbid
3. Day of month and day of week together
In standard cron, when both day-of-month and day-of-week are restricted (neither is *), the job runs when either field matches. POSIX spells this out: 0 0 1,15 * 1 runs on the 1st and 15th of each month as well as on every Monday. It does not mean "the 1st or 15th, but only if it is a Monday."
# Runs on the 1st of every month and also on every Monday
0 0 1 * 1
# To run only on the 1st when it is a Monday, test the day in the command
0 0 1 * * [ "$(date +\%u)" = "1" ] && /scripts/job.sh
AWS and Quartz avoid the surprise by refusing the combination: one day field must be ?.
4. Lost output
When a job writes output that you do not redirect, cron mails it to the crontab's owner, which only helps if the machine has a working mail system. On many servers and containers it does not, so errors disappear. Send output to a log file, or set MAILTO when mail is configured. If your logs record Unix timestamps, the Timestamp Converter turns them into readable dates.
# Silent failures if no mail system exists
0 2 * * * /scripts/backup.sh
# Log standard output and errors
0 2 * * * /scripts/backup.sh >> /var/log/backup.log 2>&1
# Or mail the output to an address
MAILTO=admin@example.com
0 2 * * * /scripts/backup.sh
5. A different environment
Cron starts jobs with a small environment: SHELL is /bin/sh, HOME and LOGNAME come from your account, and PATH is only guaranteed to find the standard utilities. Your .bashrc and .profile are not loaded, which is why a script that works in your terminal can fail under cron. Use absolute paths, set variables at the top of the crontab, and do not depend on the working directory.
PATH=/usr/local/bin:/usr/bin:/bin
NODE_ENV=production
0 9 * * * /usr/local/bin/node /home/user/app/send-report.js
6. An unescaped percent sign
In a crontab command, an unescaped % is turned into a newline: the command stops at the first one and the rest is passed to it as standard input. A line such as date +%F therefore breaks. Escape each one with a backslash, as in date +\%F.
More helpers for scripts and configuration files are in our developer tools collection.
Frequently Asked Questions
What is the smallest interval I can schedule with cron?
One minute. Cron examines its tables once a minute, so * * * * * is the fastest schedule and a five-field expression cannot run every 30 seconds. For sub-minute jobs, use a systemd timer (for example OnUnitActiveSec=30s, and lower AccuracySec from its one-minute default if you need precise timing) or a long-running process with its own timer. GitHub Actions is stricter: its shortest scheduled interval is once every 5 minutes.
How do I list and manage cron jobs?
Run crontab -l to list your jobs, crontab -e to edit them and crontab -r to remove all of them. Removal does not ask for confirmation unless you add -i. As root, add -u username to work on another user's table. System-wide jobs live in /etc/crontab and /etc/cron.d/, where each line has an extra field naming the user to run as. Many distributions also run scripts placed in /etc/cron.daily/, /etc/cron.weekly/ and /etc/cron.monthly/.
Why does my cron job work in the terminal but fail when scheduled?
Almost always the environment. Cron starts jobs with a small set of variables and does not load your shell profile, so PATH is short and your aliases and exports are missing. Use absolute paths for every command and file (for example /usr/local/bin/python3), set PATH at the top of the crontab, and do not rely on the working directory. Redirect output to a log file so you can read the actual error.
What is the difference between 5-field and 6-field cron?
Five-field cron (Linux crontab, GitHub Actions, Kubernetes) starts at the minute. Quartz and Spring put a seconds field first, which makes six fields, and Quartz accepts an optional seventh for the year. AWS EventBridge also has six fields, but they start at the minutes and end with a year. Day-of-week numbering differs as well: Linux accepts 0 to 7 with Sunday as 0 or 7, Quartz and AWS use 1 to 7 with Sunday as 1, and Spring follows Linux. Day names such as MON avoid the problem. Our Cron Expression Parser reads the standard five-field format.
How do daylight saving time changes affect cron jobs?
It depends on the scheduler, so test yours or avoid the problem. The cron(8) manual page for cronie and Debian's cron says clock changes of under three hours are handled specially for jobs that run at a fixed time or less often than hourly: after a spring-forward change the skipped jobs run right away, and after a fall-back change a job does not run twice. EventBridge Scheduler skips a job that lands in the missing hour and runs a repeated hour only once, and GitHub Actions moves a skipped 2:30 AM job to 3:00 AM when you set a time zone. The safest choice for critical jobs is UTC, or a time outside the 1:00 to 3:00 AM local window.
Sources
- The Open Group: crontab, POSIX.1-2024 (fields, day matching rule, default environment, percent sign)
- Linux man-pages: crontab(5) for cronie (steps, names, macros, CRON_TZ, MAILTO)
- Debian manpages: crontab(5) for Debian's cron (limitations: no per-user time zones)
- Linux man-pages: cron(8) for cronie (daylight saving time handling)
- Linux man-pages: crontab(1) for cronie (-l, -e, -r, -i, -u)
- Linux man-pages: flock(1) (the -n option)
- Linux man-pages: systemd.timer(5) (OnUnitActiveSec, AccuracySec)
- Linux man-pages: systemd.time(7) (calendar events, time zone suffix, weekday ranges)
- AWS: EventBridge scheduled rules, cron and rate expressions (UTC, six fields, the ? rule)
- AWS: EventBridge Scheduler schedule types (time zones and daylight saving time)
- GitHub Docs: the schedule event (syntax, timezone, 5-minute minimum, 60-day disabling, delays)
- GitHub Changelog: timezone support for scheduled workflows (March 19, 2026)
- Kubernetes: CronJob (schedule syntax, timeZone, concurrencyPolicy)
- Quartz Scheduler: CronTrigger tutorial (fields, ?, L, W, #, day-of-week numbering)
- Spring Framework: CronExpression (six fields, day-of-week numbering)