← Blog Β· May 23, 2026 Β· dev, ops

Cron Expressions: Every Pattern You'll Actually Need

A cron expression is 5 space-separated fields β€” minute, hour, day-of-month, month, day-of-week β€” and the rule that the job runs when all fields match the current time, except for the day fields where the rule flips to either. That last bit is where most cron bugs come from. The rest is memorization.

The five fields

 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ minute       (0 - 59)
 β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ hour       (0 - 23)
 β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ day of month (1 - 31)
 β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ month   (1 - 12)
 β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ day of week (0 - 6, Sunday = 0)
 β”‚ β”‚ β”‚ β”‚ β”‚
 * * * * *

Some cron flavors (Quartz, AWS EventBridge, Spring) add a seconds field at position 0 and a year field at position 6, making 6 or 7 fields total. Unix cron, Vixie cron, and most Linux distributions use the 5-field form. Check your scheduler's docs before copying an expression β€” a 5-field expression in a 6-field system means "every second."

The 7 special characters

CharacterMeansExample
*Every value in the field* * * * * β€” every minute
,List of values0,15,30,45 β€” quarter hours
-Range9-17 β€” 9am to 5pm inclusive
/Step*/5 β€” every 5 (minutes, etc.)
?No specific value (Quartz only)For day-of-month or day-of-week
LLast (Quartz/AWS)L in day-of-month = last day
#Nth weekday (Quartz)2#1 = first Monday

The 12 patterns that cover 95% of jobs

* * * * *           Every minute
*/5 * * * *         Every 5 minutes
*/15 * * * *        Every 15 minutes
0 * * * *           Every hour, on the hour
0 */2 * * *         Every 2 hours
0 0 * * *           Daily at midnight UTC
0 9 * * *           Daily at 9:00 AM UTC
0 9 * * 1-5         Weekdays at 9:00 AM (Mon-Fri)
0 0 * * 0           Weekly, Sunday midnight
0 0 1 * *           Monthly, 1st at midnight
0 0 1 1 *           Yearly, Jan 1st at midnight
0 0 1 */3 *         Quarterly, 1st of every 3rd month

Translate any cron expression to plain English with our cron expression decoder. It also shows the next 5 fire times β€” the fastest way to spot a bug.

The day-of-month vs day-of-week trap

Normal cron rule: a job runs when every field matches. The day fields break this rule. If either day-of-month or day-of-week is *, the other field is used alone. If both are specific values, the job runs when eithermatches β€” not both.

0 0 1 * 1           # NOT "1st of the month, only if Monday"
                    # ACTUALLY "Every 1st of the month, AND every Monday"
                    # = ~5-6 fires per month, not 0-1

This is a documented quirk of Vixie cron β€” see crontab(5) section "Note: The day of a command's execution can be specified by two fields…". Quartz and AWS EventBridge fix this by requiring one of the two day fields to be ? instead of *.

Timezone β€” the other silent killer

  1. Unix cron uses the system timezone of the machine it runs on.
  2. Docker containers default to UTC unless you set TZ.
  3. GitHub Actions, AWS EventBridge, and most cloud schedulers run in UTC.
  4. Kubernetes CronJobs run in UTC unless spec.timeZone is set (1.27+).

"9 AM daily" means 9 AM in whose timezone? If you live in Sydney and your scheduler is UTC, 0 9 * * * fires at 7 PM or 8 PM local depending on daylight saving. Always confirm the timezone of the scheduler, not your laptop. Convert with our timezone converter.

Quartz-only patterns (Spring, AWS, Jenkins)

0 0 12 L * ?        # Noon on the last day of every month
0 0 12 ? * 6L       # Noon on the last Friday of every month
0 0 12 ? * 2#1      # Noon on the first Monday of every month
0 0 12 ? * 2#3      # Noon on the third Monday (payroll!)
0 0 0 LW * ?        # Midnight on the last weekday of the month

These do not work in Unix cron. L, W, and # are Quartz extensions. AWS EventBridge supports them. Vixie cron does not.

Drift, misfires, and the "every X" lie

*/7 * * * * does not mean "every 7 minutes." It means "at minutes 0, 7, 14, 21, 28, 35, 42, 49, 56." The gap between minute 56 and the next fire (minute 0 of the next hour) is 4 minutes, not 7. For true evenly-spaced runs, use a list (0,7,14,21,28,35,42,49,56) and accept the boundary skip, or pick a divisor of 60 (1, 2, 3, 4, 5, 6, 10, 12, 15, 20, 30).

Validating before you ship

  1. Decode the expression to English β€” does the description match your intent?
  2. List the next 5-10 fire times β€” do they look right?
  3. Confirm the scheduler's timezone.
  4. Check whether your scheduler is 5, 6, or 7 fields.
  5. Re-check the day-of-month / day-of-week interaction if both are specified.

Tools

← All blog posts