العودة إلى المدونة
tutorial 2026-06-14

Cron Expression Parser Explained for Non-DevOps (2026)

Cron Expression Parser Explained for Non-DevOps

You ship a small feature, you need to run it every weekday at 9 AM, and you reach for cron. After three failed attempts and one production incident (the job ran every minute for an entire weekend), you're now consulting a cron expression syntax reference for the fourth time this month. The Wikipedia table is dense, the StackOverflow answers contradict each other, and the answer your AI assistant gave you doesn't quite match what your scheduler actually accepted.

This guide explains cron expressions from first principles for people who don't write them every day — product managers, founders, designers, analysts, and backend developers who use cron a handful of times per year and want to get it right without memorising arcana. We'll cover the Ai2Done Cron Parser for visualising any expression in plain English, plus the gotchas that bite even experienced engineers.

TL;DR

  • A cron expression is 5 fields: minute hour day-of-month month day-of-week — separated by spaces.
  • The trickiest gotcha: when both day-of-month and day-of-week are specified, classic Unix cron treats them as OR (runs if either matches), which is rarely what you want.
  • Use the Ai2Done Cron Parser to translate any expression to plain English and see the next 5-10 run times before deploying.
  • Timezone matters: most cron schedulers run in UTC; specify your timezone explicitly in the calling system (Kubernetes CronJob, AWS EventBridge, etc.).
  • Special characters * / , - L W ? # mean different things in classic cron vs Quartz cron — pick the right dialect for your scheduler.

Why this is harder than it looks

The original Unix cron from 1975 used 5 fields. Modern schedulers (Quartz, Kubernetes CronJobs, AWS EventBridge, GitLab schedules, GitHub Actions) extend or modify the syntax in incompatible ways. There are at least 4 widely-used dialects:

  1. Classic Unix cron (5 fields, used by crontab on Linux/macOS)
  2. Vixie cron (the de facto Linux implementation, with some extensions)
  3. Quartz cron (7 fields including seconds and year, used by Java schedulers and AWS EventBridge)
  4. Kubernetes CronJob (5 fields, but no ? and slightly different DST handling)

A cron expression that works perfectly in one system can fail validation, run at unexpected times, or silently never fire in another. The "is this expression valid?" question is genuinely dialect-dependent.

The second source of confusion: the "5 fields" framing assumes you remember the order. Most engineers under pressure remember "there's a minute and an hour and... was it day or month first?" The correct order is minute, hour, day-of-month, month, day-of-week.

The third source: special characters interact in non-obvious ways. 0 0 1,15 * 1-5 runs on the 1st and 15th of every month, OR Monday through Friday (because classic cron OR's day-of-month and day-of-week). To get "1st and 15th, but only if it's a weekday," you need Quartz cron's ? placeholder.

A good cron parser shows you the next 5-10 actual run times in your timezone, so you can sanity-check what the expression actually does before deploying it to production.

The 5 fields explained

*   *   *   *   *
│   │   │   │   │
│   │   │   │   └── Day of week (0-6 or SUN-SAT; some dialects use 1-7)
│   │   │   └────── Month (1-12 or JAN-DEC)
│   │   └────────── Day of month (1-31)
│   └────────────── Hour (0-23)
└────────────────── Minute (0-59)

Each field can be:

  • A specific number: 5 (means "this exact value")
  • A list: 1,15,30 (means "any of these values")
  • A range: 1-5 (means "from this through that")
  • A step: */15 (means "every Nth value starting from 0") — so */15 in the minute field is "0, 15, 30, 45"
  • A wildcard: * (means "every value")

Common patterns once you know the syntax:

  • * * * * * — every minute (the famous "I forgot to specify a schedule" expression that wakes you up at 3 AM)
  • */5 * * * * — every 5 minutes
  • 0 * * * * — every hour, on the hour
  • 0 9 * * * — every day at 9:00 AM
  • 0 9 * * 1-5 — weekdays at 9:00 AM (the textbook "business hours" job)
  • 0 0 1 * * — first day of every month at midnight
  • 0 0 * * 0 — Sunday at midnight (week boundary)
  • 15 14 1 * * — 2:15 PM on the first of every month
  • 0 */6 * * * — every 6 hours (0, 6, 12, 18)

Method 1: Ai2Done Cron Parser (visual verification before deploy)

The Ai2Done Cron Parser translates any expression to plain English and shows the next 10 actual fire times. The flow:

  1. Open /tools/cron_parser in any browser.
  2. Paste your expression — e.g. 0 9 * * 1-5.
  3. Instantly see:
    • Plain English: "At 09:00 AM, Monday through Friday"
    • Next 10 fire times in your local timezone
    • A field-by-field breakdown showing what each part matches
    • Validation errors for impossible expressions (e.g. 0 9 32 * * — there's no 32nd day of the month)
  4. Switch dialect between classic cron, Quartz, AWS EventBridge, and Kubernetes if your scheduler uses a non-standard variant.

The tool runs entirely client-side using the cronstrue library for human-readable translation and cron-parser for next-fire calculation. Your expression never touches a server.

Pro tip: before deploying any cron-driven job to production, paste the expression into the parser and confirm at least the next 3 fire times match what you intend. This catches the classic "I meant every 6 hours but typed 0 6 * * * which means once per day at 6 AM" error in 5 seconds.

Method 2: The crontab.guru website

If you're already in a browser, crontab.guru is the unofficial standard for cron explanation. It's a single-page web tool with the same core feature — paste expression, see English. Differences from the Ai2Done parser:

  • Runs server-side (your expression is logged on their access log)
  • Doesn't show the field-by-field breakdown
  • Only supports classic cron (no Quartz, no AWS EventBridge dialect)
  • Has been free and excellent for ~10 years

For non-sensitive expressions it's a fine choice. For expressions that might leak information (e.g. an expression containing custom field names in some Quartz dialects), the local parser is safer.

Method 3: Test in a dry-run scheduler

For high-stakes cron jobs (data pipelines, billing runs, customer-facing scheduled actions), the highest-confidence approach is to deploy to a dry-run environment first:

  • Kubernetes: deploy the CronJob with suspend: true and inspect the calculated next runs via kubectl describe.
  • AWS EventBridge: create the rule with no targets and look at the calculated NextInvocations.
  • Airflow / Dagster: use the scheduler's UI which always shows the next 5-20 fire times for any DAG schedule.

This catches edge cases that a parser doesn't (your cluster's UTC offset, daylight saving transitions, scheduler-specific quirks like AWS EventBridge's cron(0 9 ? * MON-FRI *) syntax that requires 6 fields with a ?).

The classic gotchas

Day-of-month and day-of-week are OR, not AND. 0 0 1 * 1 runs at midnight on the 1st of every month, and every Monday. Not "the 1st only if it's a Monday." To get AND, use Quartz cron's ? to indicate "no specific value" — 0 0 1 * ? runs only on the 1st regardless of weekday.

*/N is not "every N units." It's "starting from 0, every N units." So */30 * * * * runs at :00 and :30 of every hour, not "30 minutes after the last run." For real fixed-interval scheduling, use a job queue (Celery, BullMQ, Sidekiq) — cron is fundamentally calendar-based, not interval-based.

Timezone defaults are unreliable. Linux cron runs in the system timezone. Docker containers typically default to UTC. AWS EventBridge defaults to UTC. Kubernetes CronJobs default to the API server's timezone (often UTC). If your job needs to run at "9 AM local time," explicitly configure the timezone — don't trust defaults.

Daylight Saving Time creates ghost runs. When clocks spring forward at 2 AM, a job scheduled for 2:30 AM doesn't run that day. When clocks fall back, it runs twice. Most schedulers have options to handle this (Quartz's misfire policies, Kubernetes' concurrencyPolicy); read the docs for yours.

0 0 31 2 * — runs on February 31st, which doesn't exist. Most schedulers silently skip it. Test before deploying.

How we built the parser (technical deep-dive)

The Ai2Done Cron Parser is built on:

  • cronstrue for English translation (best-in-class, supports all major dialects, ~30 KB gzipped)
  • cron-parser for calculating next fire times (handles DST, leap days, ranges, steps correctly)
  • A small dialect adapter to translate AWS EventBridge syntax (cron(0 9 ? * MON-FRI *) — 6 fields, year optional) into the underlying parser's expected format
  • Web Crypto / SubtleCrypto is unused — there's no security-sensitive content here; the parser is just convenience tooling

The interesting design choice: we deliberately don't offer "construct a cron from a Google Calendar-style picker." That kind of UI is great for first-timers but tends to teach people the picker, not the underlying syntax. Cron is dense but learnable in 20 minutes; we'd rather help you read it than hide it.

FAQ

Q: Why are there 5 fields in some schedulers and 6 or 7 in others? A: Classic Unix cron has 5 fields (minute hour day month weekday). Quartz adds 2 more: seconds at the front and year at the end (* * * * * * *). AWS EventBridge uses 6 fields with day-of-week/day-of-month requiring one to be ?. Pick the dialect that matches your scheduler.

Q: What does ? mean in cron expressions? A: It's a Quartz / AWS EventBridge extension meaning "no specific value." Used in day-of-month and day-of-week to disambiguate when only one of them should match. Not supported in classic Unix cron.

Q: What does L mean in cron expressions? A: A Quartz extension for "last." L in day-of-month means "last day of the month" (28th/29th/30th/31st depending). 5L in day-of-week means "last Friday of the month." Useful for end-of-month jobs without hard-coding 28/29/30/31.

Q: What does W mean in cron expressions? A: A Quartz extension for "weekday." 15W in day-of-month means "the weekday nearest to the 15th" — useful for "monthly payroll, but never on a weekend."

Q: How do I run a job every 5 minutes during business hours only? A: */5 9-17 * * 1-5 — every 5 minutes, hours 9-17, weekdays. Verify with the parser; the next 10 fire times should match your expectation.

Q: My job ran twice during the daylight saving transition. How do I fix that? A: This is scheduler-specific. Kubernetes CronJob's concurrencyPolicy: Forbid prevents overlapping runs. Quartz has misfire policies. AWS EventBridge handles DST by always running in UTC (so daylight saving is your application's problem).

Q: Can I express "the first Monday of every month" in cron? A: In classic Unix cron, no — you need application-level logic. In Quartz cron, yes: 0 0 0 ? * MON#1 (run at midnight on the 1st Monday of every month).

Try it now

Verify any cron expression before it runs in production:

Open the Cron Parser →

Paste, see, deploy. No upload, no signup, your expressions stay in your browser.

Related reading


Last updated 2026-06-14. The Cron Parser runs 100% in your browser — your expressions never leave your device. We have no server log of what you tested.