Eternalsix

Cron expressions, field by field: a practical guide with real examples

Five little fields control almost every scheduled job on the internet. Here's how to read and write them without guessing.

Somewhere in your infrastructure, something runs on a schedule: a nightly backup, an hourly cache warm, a weekly report, a "clean up stale sessions every fifteen minutes" job. Odds are overwhelming that the schedule is written as a cron expression — five terse fields that have driven Unix scheduling since the 1970s and now power everything from Linux crontab to Kubernetes CronJobs to GitHub Actions to serverless triggers.

Cron syntax is compact to the point of being cryptic. */15 9-17 * * 1-5 is completely opaque until someone explains the grammar — and then it's obvious. Let's make it obvious.

The five fields

A standard cron expression is five fields separated by spaces:

┌───────────── minute        (0 - 59)
│ ┌───────────── hour        (0 - 23)
│ │ ┌───────────── day of month (1 - 31)
│ │ │ ┌───────────── month     (1 - 12)
│ │ │ │ ┌───────────── day of week (0 - 6, Sunday = 0)
│ │ │ │ │
* * * * *

Read left to right: minute, hour, day-of-month, month, day-of-week. Each field answers "at which values of this unit should the job fire?" A * means "every value." So * * * * * means every minute of every hour of every day — the job runs 1,440 times a day.

The four operators

Every field understands the same four operators, and once you know them you can read any expression.

* — every value. * in the hour field means "every hour."

A specific number. 0 in the minute field means "at minute zero" (the top of the hour). 30 3 * * * runs at 03:30 every day.

A list with commas. 0,15,30,45 in the minute field means "at 0, 15, 30, and 45 minutes past." 0 9,12,17 * * * runs at 9am, noon, and 5pm.

A range with a hyphen. 1-5 in the day-of-week field means Monday through Friday. 0 9-17 * * * runs at the top of every hour from 9am to 5pm.

A step with a slash. */15 in the minute field means "every 15th value" — i.e., 0, 15, 30, 45. You can combine steps with ranges: 0-30/10 means 0, 10, 20, 30. Steps are the operator people forget exists, and they're the cleanest way to express "every N units."

Reading real expressions

Now the earlier example decodes cleanly:

*/15 9-17 * * 1-5

Every 15 minutes, during hours 9 through 17, on days of the week Monday through Friday. In plain English: every 15 minutes, 9am–5:45pm, on weekdays. A business-hours polling job.

A few more you'll recognize once you can read them:

ExpressionMeaning
0 0 * * *Every day at midnight
0 */6 * * *Every 6 hours (00:00, 06:00, 12:00, 18:00)
0 9 * * 19am every Monday
30 2 1 * *02:30 on the 1st of every month
0 0 * * 0Midnight every Sunday
15 14 1 * *14:15 on the 1st of every month
0 22 * * 1-510pm on weekdays

The day-of-month / day-of-week gotcha

Here's the rule that surprises almost everyone. When you specify both day-of-month and day-of-week (neither is *), most cron implementations treat them as an OR, not an AND.

0 0 1,15 * 1

You might read that as "midnight on the 1st and 15th, but only if it's a Monday." It actually means "midnight on the 1st, and the 15th, and every Monday." The job fires on any day that matches either condition. If you need a true "the 15th only when it's a Monday," cron can't express it directly — you check the extra condition inside the job itself.

Special strings and non-standard fields

Many cron implementations accept convenient shorthand macros:

Some flavors add a sixth field for seconds at the front (common in Quartz, Spring, and many programming-language cron libraries), so */30 * * * * * means "every 30 seconds." And some support L for "last" (last day of month), W for "nearest weekday," and # for "the Nth weekday" (5#3 = the third Friday). These are extensions — always check what your specific scheduler supports rather than assuming.

The timezone trap

Cron traditionally runs in the server's local timezone (or historically, UTC, or whatever TZ is set to). This is a classic source of "the report went out at the wrong time" bugs, especially around daylight saving transitions, when a local-time schedule can either skip an hour or run a job twice. Two defensive habits: pin your scheduler to UTC and do timezone math inside the job, or explicitly configure the timezone your platform offers (Kubernetes CronJobs, for instance, added a timeZone field for exactly this reason). Either way, know which timezone your cron is interpreting, because the expression itself carries no timezone information.

Sanity-checking before you deploy

The failure mode with cron isn't usually a syntax error — it's a schedule that parses fine but doesn't mean what you intended. 0 0 * * * and 0 * * * * differ by a single character and by a factor of 24 in how often they fire. Before you commit a schedule, it's worth translating it back into English and checking the next few run times. A Cron Expression Explainer does exactly that: paste the expression, read a plain-language description, and confirm it lines up with what you meant before it ships to production.

The takeaway

Cron is five fields — minute, hour, day-of-month, month, day-of-week — and four operators: * for all, a number for one, commas for a list, hyphens for a range, and slashes for steps. The two things that bite even experienced engineers are the day-of-month/day-of-week OR behavior and the timezone the scheduler interprets. Keep those in mind, translate any expression back to plain English before you trust it, and cron stops being cryptic.