Every cron expression is five fields separated by spaces, and every field controls one unit of time: minute, hour, day of the month, month, and day of the week, in that exact order. That's the whole format. The reason it looks intimidating is that each field can hold several different kinds of value (a number, a wildcard, a range, a list, a step), and crontab syntax gives you almost no visual cues for which one you're looking at. 0 2 * * * and */2 0 * * * use the same five slots but mean completely different things, and there's nothing in the shape of the string to warn you.
Once you know what each position means, though, reading an unfamiliar expression stops being guesswork. Here's the field order, left to right, with the range of values each one accepts:
| Field | Allowed values | Notes |
|---|---|---|
| Minute | 0–59 | First field, always |
| Hour | 0–23 | 24-hour clock, no AM/PM |
| Day of month | 1–31 | No 0; months with fewer days just never match on the missing dates |
| Month | 1–12 | Some implementations also accept Jan–Dec |
| Day of week | 0–6 | 0 is Sunday; some systems also accept 7 as Sunday |
So 0 2 * * * reads as: minute 0, hour 2, any day of the month, any month, any day of the week. Every day at 2:00 AM. That's the core skill for writing a cron job schedule: fill in the field you care about, leave the rest as a wildcard.
The special characters, one at a time
Crontab syntax gives you four operators beyond a plain number, and they combine in ways that cover almost every schedule you'd actually want.
*: any value. It means "don't restrict this field." * * * * *, all five fields wide open, runs once a minute, forever. That's the loosest expression you can write, and it's a good one to remember as the default before you start narrowing fields down.
,: a list. 0 9,17 * * * runs at 9:00 AM and again at 5:00 PM, every day. You can list as many values as the field allows; there's no hard limit on list length.
-: a range, inclusive on both ends. 0 9 * * 1-5 runs at 9:00 AM Monday through Friday, because the day-of-week field is restricted to 1 through 5 (Monday to Friday) and everything else stays wide open. Ranges and lists combine too: 1-5,7 in a field means "1 through 5, plus 7."
/: a step value, and the one people misread most often. */15 in the minute field doesn't mean "at minute 15"; it means "every 15 units starting from the base of the range," so it fires at :00, :15, :30, and :45. Swap the base to 10-40/10 and the step applies within that range instead of the whole field, giving you 10, 20, 30, 40. This is also the field type behind the site's cron expression generator, which will decompose any expression you paste in this way and show you which matches it produces.
Cron expression examples for schedules people actually need
Most real-world cron jobs fall into a small set of patterns. Here are the ones that come up constantly, worth having memorized or bookmarked rather than reverse-engineered every time:
| Schedule | Expression |
|---|---|
| Every 5 minutes | */5 * * * * |
| Every day at 2:00 AM | 0 2 * * * |
| Every Monday at 9:00 AM | 0 9 * * 1 |
| First day of every month, midnight | 0 0 1 * * |
| Every weekday at 9:00 AM | 0 9 * * 1-5 |
Notice the pattern: you're almost always restricting one or two fields and leaving the rest as *. "Every weekday" doesn't touch the hour or minute logic at all; it's the same 9:00 AM as the Monday-only example, just with a wider day-of-week range. That's the mental model worth keeping: figure out which field(s) actually change for your schedule, and leave the others alone.
The day-of-month vs. day-of-week gotcha
This is the one that catches people who've been writing cron jobs for years, not just beginners. If you restrict both the day-of-month and day-of-week fields at the same time, standard cron does not require both to match; it treats them as OR, and the job fires if either one matches. Someone reads 0 0 15 * 5 expecting "the 15th, but only if it's a Friday," and instead gets a job that runs on every 15th of the month and on every Friday. I've watched a deploy script fire on an unrelated Friday because of exactly this, and the fix wasn't in the script — it was in the schedule.
The workaround, if you genuinely need an AND relationship (the 15th, only when it lands on a Friday), is to leave both fields open and add the date check inside the job itself. Cron's five fields just can't express that condition directly. The one exception to the OR rule: if either day field is left as *, it's ignored and the other field alone decides, which is why "every weekday" and "1st of the month" examples above work exactly as expected. The OR behavior only kicks in once you restrict both simultaneously.
Timezones: cron's other quiet trap
A cron expression has no timezone information baked into it. It's just five fields of numbers. What timezone those numbers get evaluated in depends entirely on the system running the job. A traditional crontab on a Linux server runs in whatever local timezone that server is configured for, which is often UTC on cloud instances but not always, and can silently differ between a staging box and production if someone provisioned them at different times or in different regions. Scheduled task systems in CI platforms, cloud functions, and job runners each have their own default, and some let you override it per-job while others don't. "0 2 * * *" scheduled with the assumption of Eastern time and actually evaluated in UTC will run at 2:00 AM UTC, which is 9:00 PM or 10:00 PM Eastern the night before, not 2:00 AM Eastern at all.
The DST edge case is the one that actually breaks things in production, and it's worth knowing the specific mechanics rather than a vague "watch out for daylight saving." On a system using local time with DST enabled, a job scheduled at 2:30 AM can either not run at all on the spring-forward night (the clock jumps from 1:59 AM straight to 3:00 AM, so 2:30 never occurs), or run twice on the fall-back night, because 1:00 AM to 2:00 AM happens twice as clocks roll back. Servers configured to run in UTC sidestep this entirely, since UTC never observes daylight saving, which is the main practical reason ops teams standardize infrastructure on UTC rather than a local timezone. If a job scheduling in your stack ever runs twice a year for no obvious reason, check whether it lands in that 1:00–2:00 AM window before assuming it's an application bug.
Try it
GlaeKit's Cron Expression Generator parses any expression you type and shows a plain-English description alongside the next five run times, so you can sanity-check a schedule before you commit it to a crontab. Everything runs in your browser; nothing is uploaded.
Frequently asked questions
What does */5 mean in a cron expression?
It's a step value, meaning "every 5 units of this field, starting from the field's base." In the minute field,*/5 means the job fires at :00, :05, :10, :15, and so on through :55, twelve times an hour.
How do I run something every weekday?
Restrict the day-of-week field to a range covering Monday through Friday and leave day-of-month as a wildcard: 0 9 * * 1-5 runs at 9:00 AM every weekday. Since day-of-month stays open, there's no OR-gotcha to worry about here.
What timezone does cron use?
Whatever local timezone the host system is set to, unless the scheduler you're using explicitly lets you configure one. There's no timezone information in the expression itself, just five numeric fields, so the same expression can mean different wall-clock times depending on where it runs.
Why did my job run twice on the same day?
Two common causes: the day-of-month and day-of-week fields are both restricted, which cron treats as OR and can produce more matches than expected, or the job is scheduled inside the daylight-saving "fall back" hour, which repeats once a year on systems running local time.
Can I restrict both day-of-month and day-of-week at once?
You can write it, but standard cron won't AND the two fields together; it ORs them, running the job when either matches. If you need a true AND (a specific date that also has to fall on a specific weekday), leave one field as a wildcard and check the condition inside the job itself.