CronSage
Unlimited Free Usage

Cron Expression Parser & Explainer

Paste any cron expression and instantly get a plain-English explanation, the next & previous run times, a would-it-fire-at-this-time diagnostic, DST skip / double-fire warnings and overlap-risk checks. No signup. No paywall. Everything runs in your browser.

cronsage — explainer console
Try:

Cron Expressions Explained: The Complete Guide to Scheduling Jobs Like a Pro

If you have ever stared at a string like */15 * * * * and wondered what on earth it means, you are not alone. Cron expressions are one of those things every developer, DevOps engineer and system administrator eventually has to learn — and almost everyone learns them the hard way, usually at 2 a.m. when a job fires at the wrong time. This guide explains cron expressions from the ground up: what each field does, how the special characters work, where the classic traps hide, and how the Quartz and GitHub Actions dialects differ from standard cron.

What is a cron expression?

A cron expression is a compact text string that describes a recurring schedule. The name comes from cron, the time-based job scheduler built into Unix-like operating systems since the 1970s. You write the expression, the scheduler reads it, and your command, script or workflow runs whenever the current time matches the pattern.

Today cron syntax lives far beyond Linux servers. You will find it in Jenkins build triggers, Kubernetes CronJobs, the Java Quartz scheduler, cloud platforms like AWS EventBridge, and CI systems like GitHub Actions. The core idea is identical everywhere: five (sometimes six or seven) fields, each describing one unit of time, read left to right from the smallest unit to the largest.

The reason cron expressions confuse people is density. Five numbers and asterisks carry a surprising amount of meaning, and two expressions that look almost identical — 0 9 * * * versus 0 9 * * 1 — behave completely differently. A cron parser that translates the expression into plain English, like the console above, is the fastest way to check that what you wrote is what you meant.

The five fields, decoded

A standard cron expression has five fields separated by spaces, always in this order:

PositionFieldAllowed valuesSpecial characters
1Minute0–59* , - /
2Hour0–23* , - /
3Day of month1–31* , - /
4Month1–12 or JAN–DEC* , - /
5Day of week0–7 or SUN–SAT* , - /

So 30 2 * * * reads as: minute 30, hour 2, every day of the month, every month, every day of the week — in plain English, "at 02:30 every day". And 0 9 * * 1-5 means "at 09:00, Monday through Friday". The fields are always evaluated together: a job fires only when all the field conditions are satisfied at the same time — with one famous exception involving the two day fields, which we will get to shortly.

Note that hours use the 24-hour clock. 0 14 * * * is 2 p.m., not 2 a.m. There is no AM/PM marker in cron, which is another classic source of midnight-versus-noon confusion: 0 0 * * * is midnight, 0 12 * * * is noon.

Special characters: the *, ,, - and / operators

Four special characters do all the heavy lifting in cron expressions. Master these and you can read any schedule:

Asterisk (*) — "every". A star means "every possible value for this field". * * * * * therefore means "every minute of every hour of every day" — the most frequent schedule cron can express. In the minute field, 0 * * * * means "at minute zero of every hour", i.e. once per hour.

Comma (,) — lists. Separate individual values: 0 9,18 * * * runs at 09:00 and 18:00 every day. 0 0 * * 1,3,5 runs at midnight on Monday, Wednesday and Friday.

Hyphen (-) — ranges. 0 9 * * 1-5 covers Monday through Friday. 0 0 * 1-3 * runs daily during the first quarter of the year. Ranges are inclusive on both ends.

Slash (/) — steps. The step operator picks every nth value: */15 * * * * means "every 15 minutes" (minutes 0, 15, 30, 45). 0 */2 * * * means "every 2 hours". You can combine steps with ranges too: 0 9-17/2 * * * fires at 09:00, 11:00, 13:00, 15:00 and 17:00 — every two hours during the working day.

One subtle point: */10 in the minute field starts counting from 0, giving minutes 0, 10, 20, 30, 40, 50. If you want "10 minutes past every hour" instead, write the literal 10 * * * *. The step always anchors at the field's minimum unless you give it an explicit range like 5-55/10.

Names instead of numbers

In the month and day-of-week fields you can use three-letter English names instead of numbers: JAN, FEB, MAR … DEC and SUN, MON, TUE, WED, THU, FRI, SAT. Names are case-insensitive, so 0 9 * * mon-fri works exactly like 0 9 * * 1-5. They work inside ranges and lists as well: 0 0 1 JAN,APR,JUL,OCT * runs at midnight on the first day of each quarter.

A small quirk worth knowing: in standard cron, both 0 and 7 mean Sunday in the day-of-week field. So 0 0 * * 0 and 0 0 * * 7 are identical. (Quartz does this differently — more on that below.)

The day-of-month vs day-of-week trap: the OR rule

Here is the single most misunderstood behavior in all of cron. When both the day-of-month and day-of-week fields are restricted (neither is *), the job runs when either condition matches — not when both match. This is the OR rule, and it surprises almost everyone the first time.

Consider 0 0 1 * 1. A newcomer reads it as "midnight on the 1st of the month, but only if it's a Monday". What it actually means is "midnight on the 1st of every month, or every Monday" — roughly five runs a month instead of the occasional one. If you truly want "the first Monday of the month", standard cron cannot express it directly; you need a wrapper script that checks the date, or a scheduler with richer syntax.

The rule, precisely: if day-of-month is *, only day-of-week is considered; if day-of-week is *, only day-of-month is considered; if both are restricted, the job fires when either matches; if both are *, it fires every day. Paste any suspicious expression into the parser above and check the field breakdown — it spells out exactly which rule is in play.

Quartz cron: the 6- and 7-field dialects

The Quartz scheduler, popular in Java applications, extends cron with extra fields and slightly different conventions. A Quartz expression has six fields — seconds, minutes, hours, day-of-month, month, day-of-week — with an optional seventh for the year. So the Quartz equivalent of "every 15 minutes" is 0 */15 * ? * *: note the leading seconds field and the ?.

The question mark is Quartz's "no specific value" placeholder, used in exactly one of the two day fields. Because Quartz evaluates day-of-month and day-of-week with AND logic instead of cron's OR rule, it forces you to disambiguate: put ? in the day field you don't care about. 0 0 12 ? * WED means "12:00 every Wednesday"; 0 0 12 15 * ? means "12:00 on the 15th of every month". Setting both day fields to specific values is an error in Quartz, and the parser above flags it for you.

Another Quartz difference: day-of-week is numbered 1 (Sunday) through 7 (Saturday), unlike standard cron's 0–6. Quartz also supports advanced characters like L (last), W (nearest weekday) and # (nth weekday of the month, as in 3#2 for the second Tuesday). Those are powerful but rare; most teams never need them, and this parser intentionally asks you to expand them into explicit values so the plain-English explanation stays exact.

GitHub Actions schedules

GitHub Actions' schedule trigger uses standard 5-field POSIX cron syntax — the same grammar as a Linux crontab. Two practical differences matter. First, the minimum interval is five minutes; anything more frequent, like * * * * *, will not run every minute. Second, scheduled workflows run in UTC and can be delayed by several minutes during periods of high load, so never design a workflow that depends on to-the-second timing. The parser above includes a GitHub Actions mode that warns you automatically if your expression fires more often than every five minutes.

Daylight saving time: the skip and the double

Cron schedules are evaluated in wall-clock time, which means daylight saving transitions can do strange things to your jobs twice a year. In spring, when clocks jump forward (for example 02:00 → 03:00), any schedule targeting the skipped hour simply never fires that day. A backup job set for 30 2 * * * silently misses its run on the spring-forward Sunday — no error, no retry, just a missing backup you might not notice for weeks.

In autumn the opposite happens: when clocks fall back, the repeated hour occurs twice, and a job scheduled inside it can fire twice. Whether it actually double-fires depends on your scheduler — some dedupe, some don't — but the risk is real, especially for jobs that send emails, charge customers or mutate data.

The practical advice: avoid scheduling critical jobs between 01:00 and 03:00 local time, make jobs idempotent so a double run is harmless, and always verify DST behavior in the timezone your scheduler actually uses (often UTC on servers, which has no DST at all). The console above computes every run in your selected timezone and flags both skips and potential double-fires automatically — one of the few free tools that does.

Cron cheat sheet: common examples

ExpressionMeaning
* * * * *Every minute
*/15 * * * *Every 15 minutes
0 * * * *Every hour, on the hour
0 9 * * *Every day at 09:00
0 9 * * 1-5Weekdays at 09:00
30 2 * * 0Sundays at 02:30
0 0 1 * *First day of every month at midnight
0 0 1 1 *Once a year: midnight on January 1st
0 12 * * 6Saturdays at noon
0 0 * * 0Sundays at midnight

Mistakes to avoid

Swapping minute and hour. 0 9 * * * (9 a.m. daily) versus 9 0 * * * (9 minutes past midnight) — one transposed field, wildly different behavior. Always double-check field order with a parser before deploying.

Assuming the server's timezone. A crontab entry runs in the system's local timezone, which on many servers is UTC — not your laptop's timezone. A job you think runs at "9 a.m." might run at 9 a.m. UTC. When in doubt, set the timezone explicitly and verify with the next-runs list.

Overlapping runs. If a job takes 20 minutes but its schedule fires every 10, instances pile up and can exhaust memory, duplicate work or corrupt data. Use the overlap-risk check above: enter your expected job duration and it compares it against the real interval between consecutive runs.

Impossible dates. 0 0 30 2 * looks valid but February never has 30 days — the job will never run, and cron will never tell you. Similarly 0 0 29 2 * only fires in leap years. A good tester reports "no upcoming runs" instead of leaving you guessing.

Testing in production. Never deploy an untested schedule for anything important. Paste the expression into a tester, read the plain-English explanation out loud, scan the next ten run times, and run the would-it-fire diagnostic against a few specific dates — including a DST transition weekend. Thirty seconds of checking beats a 2 a.m. incident every time.

Frequently asked questions

What does */15 * * * * mean in cron?

It means "every 15 minutes". The */15 in the minute field selects minutes 0, 15, 30 and 45 of every hour, while the remaining four asterisks mean every hour, every day of the month, every month and every day of the week. So the job fires at :00, :15, :30 and :45 past each hour, around the clock.

If I set both day-of-month and day-of-week, when does my cron job run?

It runs when either condition matches — this is cron's OR rule. For example, 0 0 1 * 1 fires at midnight on the 1st of every month or every Monday, not only when the 1st is a Monday. If you need both conditions to hold simultaneously, standard cron cannot express it; use a wrapper script that checks the date, or a scheduler with richer syntax such as Quartz.

What is the difference between standard cron and Quartz cron?

Quartz adds a seconds field at the start (and an optional year field at the end), uses ? as a "no specific value" placeholder in one of the two day fields, numbers Sunday as 1 instead of 0, and evaluates day-of-month and day-of-week with AND logic rather than standard cron's OR rule. A Quartz expression like 0 */15 * ? * * is the equivalent of the standard */15 * * * *.

Can I schedule GitHub Actions workflows with cron?

Yes — the schedule trigger in a workflow file accepts standard 5-field cron expressions. Keep in mind that the shortest supported interval is 5 minutes, schedules run in UTC, and GitHub may delay runs by several minutes during high load. CronSage's GitHub Actions mode warns you automatically if your expression fires more often than every five minutes.

Why did my scheduled job skip a run or run twice?

Daylight saving time is the usual culprit. When clocks spring forward, the skipped hour never happens, so any job scheduled inside it silently misses a run. When clocks fall back, the repeated hour happens twice, so a job scheduled inside it may fire twice. CronSage detects both cases for your selected timezone and lists them as warnings above the next-runs timeline.

How can I test a cron expression before deploying it?

Paste it into the parser at the top of this page: read the plain-English explanation, scan the next and previous run times, and use the "would it fire at this time?" diagnostic to check specific dates — including a daylight-saving transition weekend. Also verify the timezone your scheduler uses (many servers run on UTC) and check the overlap-risk warning if your job takes a while to finish.