Cracking Cron: An ELI5 Guide to the Five Fields

Cron is one of those things that looks terrifying the first time you see it. You open someone's crontab and you're greeted with something like */15 6-22 * * 1-5 and your brain immediately goes: what is this alien language?

But here's the secret — cron is actually just a sentence. A weirdly formatted one, sure, but still a sentence. Once you understand the five slots and what goes in each one, you'll be writing your own schedules in ten minutes. Let's break it down like you've never touched a terminal before.

The Five Fields, Explained Like You're Five

Every cron expression has exactly five fields, separated by spaces, followed by the command you want to run. Here they are, left to right:

MINUTE  HOUR  DAY-OF-MONTH  MONTH  DAY-OF-WEEK  command

Think of it like filling out a form. You're telling the system: "Run my command when the minute is this, the hour is that, the day of the month is such, the month is so, and the day of the week is this other thing." If any field says *, that just means "I don't care — any value is fine."

Simple example: you want to send yourself an email every day at 9 AM.

0 9 * * * /usr/bin/send-email.sh

Read it: "At minute 0, at hour 9, on any day of the month, in any month, on any day of the week — run this script." That's it. That's really all cron is doing.

Field 1: Minute (0–59)

This is the most granular field. It tells cron which minute within the hour to trigger. Zero means "on the hour exactly." If you put 30, it fires at the half-hour mark.

The */ syntax means "every X." So */15 in the minute field means every 15 minutes — at :00, :15, :30, and :45. Very handy for health checks or polling jobs.

Gotcha #1: A lot of beginners put * in the minute field when they want something to run "once an hour." But * * * * * doesn't mean "once an hour" — it means every single minute. Sixty times per hour. Your server will not thank you. Always be explicit: 0 * * * * is "once an hour, at the top of the hour."

Field 2: Hour (0–23)

Cron uses 24-hour time. Midnight is 0, noon is 12, 11 PM is 23. No AM/PM here.

Ranges work here too. 9-17 in the hour field means "from 9 AM to 5 PM." Combine it with */2 for "every two hours between 9 AM and 5 PM": 0 9-17/2 * * *. That fires at 9, 11, 1, 3, and 5.

Commas let you list specific values. 8,12,18 means 8 AM, noon, and 6 PM — three discrete times, no ranges involved.

Field 3: Day of Month (1–31)

Pretty straightforward: which day of the calendar month. 1 is the first, 15 is the fifteenth, 31 is the thirty-first (for months that have it).

This is where people set up "first of the month" billing jobs or "last Saturday of the month" reports — though that second one is where things get messy, and we'll come back to it.

Gotcha #2: February. If you write 0 6 31 * * expecting something to run monthly, it will silently skip February, April, June, September, and November — because those months don't have 31 days. Cron just does nothing and moves on. No error, no log, no warning. This has bitten so many developers in production that it's practically a rite of passage.

Field 4: Month (1–12)

Month number, or on many systems you can use short names: jan, feb, and so on. A quarterly report job might look like 0 8 1 1,4,7,10 * — 8 AM on the first day of January, April, July, and October.

The asterisk here is almost always what you want unless you're doing genuinely seasonal tasks. Most recurring jobs don't care what month it is.

Field 5: Day of Week (0–7, where 0 and 7 are both Sunday)

This one surprises people: both 0 and 7 mean Sunday. It's a quirk inherited from early Unix. So Monday is 1, Tuesday is 2, and so on up to Saturday at 6. Many systems also accept three-letter abbreviations like mon, tue, sat.

Weekday ranges are really useful for business jobs. 0 9 * * 1-5 runs at 9 AM Monday through Friday and ignores weekends entirely.

The Big Gotcha: Day-of-Month vs Day-of-Week

This one deserves its own section because it confuses almost everyone who tries to do something clever.

You might think: "I want to run my cleanup job on the first Monday of every month." So you write:

0 8 1 * 1

Nope. That does not mean "the first Monday." It means "8 AM on the first of the month OR any Monday." Cron treats these two fields with an OR when both are restricted — not an AND. So if the first of the month falls on a Wednesday, the job still runs. If Monday the 7th rolls around, the job also runs. You've now got a much more frequent job than you wanted.

The only safe workaround in plain cron syntax is to do the math in your script itself. Run the job every Monday (0 8 * * 1) and at the top of the script, check: if [ $(date +%d) -gt 7 ]; then exit 0; fi. Only the first Monday will pass that check.

This is a genuine limitation of cron's design, not a bug you can configure away.

Timezones in CI: The Silent Killer

Here's one that shows up constantly in DevOps and CI/CD work. You set a cron schedule in your pipeline config (GitHub Actions, GitLab CI, CircleCI — pick your poison) and it just doesn't fire when you expect.

The culprit is almost always timezone. Most CI platforms run their cron scheduler in UTC. Your company is in IST (UTC+5:30) or PST (UTC-8) and you scheduled the nightly build for "midnight" without specifying which midnight. You end up with your "midnight" build running at 7:30 PM or 4 PM local time, which is absolutely not what you wanted.

GitHub Actions is a good example: their cron schedules run in UTC only. There's no setting to change it. If you want your job to run at 2 AM IST, you need to subtract 5 hours and 30 minutes and write 30 20 * * * (8:30 PM UTC = 2 AM IST next day).

Practical habit: Always comment your CI cron lines with both the UTC time and your local equivalent. Future-you will be grateful.

# Runs at 2:30 AM IST (9:00 PM UTC previous day)
0 21 * * * /deploy/nightly.sh

Also: CI crons are not precise to the second, or even the minute in heavy traffic. GitHub Actions explicitly warns that cron jobs can be delayed by 15–30 minutes during peak load. If you're building anything that requires exact-to-the-minute timing, cron in CI is the wrong tool. Use a dedicated scheduler or a message queue with a timer.

A Few Real-World Examples to Burn Into Your Brain

  • */5 * * * * — Every 5 minutes. Classic health check or metrics flush.
  • 0 0 * * * — Midnight every night. Nightly backups, log rotation.
  • 30 6 * * 1-5 — 6:30 AM weekdays. Morning report emails.
  • 0 */4 * * * — Every 4 hours. Cache warming, data sync from external APIs.
  • 0 9 1 * * — 9 AM on the first of every month. Monthly invoice generation (just make sure your script handles short months gracefully).
  • 59 23 31 12 * — 11:59 PM on December 31st. New Year's Eve confetti email. Because why not.

Tools That Make This Less Painful

You don't have to memorize all of this cold. crontab.guru is the gold standard for checking your expression in plain English before you deploy it. Type in your cron string and it tells you exactly when it fires next. Bookmark it. Use it every time.

If you're generating cron strings programmatically (say, letting users schedule their own jobs in your app), libraries like node-cron for Node.js or Python's croniter can parse and validate expressions for you, and croniter even lets you iterate over future fire times — super useful for showing users "your job will next run at X."

The One-Line Summary

Cron reads left to right: minute, hour, day-of-month, month, day-of-week. Stars mean "any." Slashes mean "every N." Commas list values. Dashes make ranges. The day-of-month and day-of-week fields are OR'd together, not AND'd — keep that in mind for complex schedules. And in CI, always specify your timezone offset explicitly or you'll be debugging phantom timing issues at 2 AM.

Once it clicks, you'll be using cron for everything. It's one of those tools that rewards the ten minutes you spend actually understanding it rather than cargo-culting expressions from Stack Overflow.