Developer Tools

Cron Expressions Explained: Schedule Anything

October 2026 • By Muhammad AbuBakar Azam 9 min read • Zero-Data Privacy Verified

Table of Contents

`/15 ` runs every 15 minutes. `15 ` runs once an hour, at 15 minutes past. One character — a slash and an asterisk — is the difference between 96 executions a day and 24. A developer I know once deployed a "lightweight hourly cleanup" that was actually running every minute, hammering the database 1,440 times a day instead of 24, because of a misplaced ``. The cleanup worked fine. The database did not enjoy it.

Cron expressions are tiny — five fields, a handful of special characters — and they control when the world's background jobs run: backups, reports, cache clears, billing runs, health checks. They're also one of the most misread notations in programming, because every field looks the same and the special characters change meaning subtly between positions. Let's fix that permanently.

The Five Fields, Decoded

A standard cron expression is five fields, always in this order:

`minute hour day-of-month month day-of-week`

So `30 2 *` reads: minute 30, hour 2, every day of the month, every month, every day of the week — i.e., 2:30 AM every day. Each field accepts specific values: minute is 0–59, hour is 0–23 (24-hour clock, no AM/PM), day-of-month is 1–31, month is 1–12 (or JAN–DEC), day-of-week is 0–7 (or SUN–SAT, where both 0 and 7 mean Sunday).

The special characters are where the power lives. `` means "every possible value" for that field. `,` lists specific values (`1,15` = the 1st and 15th). `-` defines a range (`1-5` = Monday through Friday in the day-of-week field). `/` defines a step (`/15` = every 15 units). Read a cron expression left to right, expanding each field in plain words, and it stops being cryptic: `0 9-17 1-5` becomes "at minute 0, hours 9 through 17, every day, every month, Monday through Friday" — once an hour during business hours on weekdays.

If you're ever unsure what an expression actually does, don't squint at it — paste it into the Cron Expression Schedule Builder & Explainer, which translates it to plain English and shows you the next several run times. Reading "next run: Friday 2:30 AM" beats re-deriving the logic in your head every time.

A clock face combined with schedule gears
A clock face combined with schedule gears

Mistake 1: `*/15` Is Not `15`

This is the mistake from the opening story, and it deserves its own section because it's so easy to make. In the minute field, `15` means "at minute 15" — once per hour. `*/15` means "every 15 minutes starting at 0" — at :00, :15, :30, and :45. The `/` turns a single point into a repeating interval.

The confusion deepens because the same visual pattern means different things in different fields. `/15` in the minute field is every 15 minutes; `/2` in the hour field is every 2 hours; `*/3` in the month field is every 3 months (quarterly). The step always applies to the field it's in. And `5/15` — starting the step at 5 instead of 0 — means :05, :20, :35, :50. You rarely need that form, but when you see it in someone else's crontab, now you know.

The defensive habit: whenever you write or review a cron expression, say the schedule out loud in words before deploying. "Every 15 minutes" vs "at 15 minutes past the hour" sound nothing alike, and hearing the wrong one catches the bug that reading the symbols missed.

Mistake 2: Day-of-Month AND Day-of-Week Actually Means OR

Here's the genuinely surprising one: when you specify both day-of-month and day-of-week as restricted values (not ``), cron treats them as OR, not AND. `0 0 1 1` does not mean "midnight on the 1st of the month if it's a Monday." It means "midnight on the 1st of the month, AND every Monday at midnight." The man page says so, but almost nobody expects it, because every other combination of fields is AND.

In practice, this bites when someone writes something like `0 9 15 1-5` intending "9 AM on the 15th, but only on weekdays" — and instead gets "9 AM on the 15th AND 9 AM every weekday." The job runs far more often than intended. The fix is simple once you know: keep one of the two fields as `` unless you genuinely want the OR behavior. If you need "the 15th but only on weekdays," cron can't express that directly — handle the weekday check inside the job itself.

Mistake 3: Forgetting Whose Clock You're On

Cron runs on the server's local time, not yours. Your laptop is in Karachi (UTC+5), the server is in Frankfurt (UTC+2 in summer), and your "run at 9 AM" job fires at what feels like noon to you. Worse, servers move: a migration from one region to another silently shifts every schedule, and daylight saving transitions make jobs run twice or skip an hour on the switchover weekends.

Three defenses. First, know your server's timezone — run `date` on it before writing any schedule, don't assume. Second, for anything where the wall-clock time matters to humans (reports, notifications, billing), consider whether the schedule should follow a specific timezone and document which one. Third, for the truly critical jobs, some schedulers (systemd timers, cloud schedulers) let you set the timezone explicitly per job — use that instead of relying on the machine default, because machine defaults change.

A timetable grid showing scheduled task times
A timetable grid showing scheduled task times

Mistake 4: Sunday Is Both 0 and 7 (and Other Off-by-Ones)

In the day-of-week field, both `0` and `7` mean Sunday — a historical quirk that confuses everyone exactly once. `1-5` is Monday to Friday, `0` or `7` is Sunday, `6` is Saturday. So `0 0 0` and `0 0 7` are the same schedule: midnight every Sunday.

The subtler off-by-one is the month and day numbering: months are 1–12 (January is 1, and there's no month 0), days of month are 1–31. Anyone coming from JavaScript's zero-indexed months will instinctively write `0` for January and get an error or a surprise. And `31` in day-of-month doesn't mean "last day of the month" — in February, `0 0 31 ` simply never runs. Cron has no "last day" concept in the classic five-field format (some extended variants add `L` for this).

The Cookbook: Schedules You'll Actually Use

Enough theory — here are the schedules that cover the vast majority of real jobs. Copy them, but say each one out loud first:

One pro tip for the hourly/daily jobs: don't schedule everything at `:00`. If every job on the server fires at minute 0, you get a thundering herd — CPU spikes, database contention, and timeouts that vanish when you investigate because the spike is over. Scatter jobs across the hour (`7`, `23`, `41`...) and the load smooths out beautifully.

Try it free — no sign-up

Run the numbers yourself in seconds. Everything happens in your browser.

Open the Cron Expression Schedule Builder & Explainer

Frequently Asked Questions

What do the 5 fields in a cron expression mean?

In order: minute (0–59), hour (0–23), day of month (1–31), month (1–12), day of week (0–7, where 0 and 7 are both Sunday). `30 2 ` means 2:30 AM daily. Special characters: `` (every), `,` (list), `-` (range), `/` (step).

What is the difference between */15 and 15 in cron?

In the minute field, `15` means "at minute 15" (once per hour); `/15` means "every 15 minutes" (:00, :15, :30, :45). The `/` creates a repeating step from the `` wildcard. Mixing these up is one of the most common cron bugs — always read the schedule aloud in words before deploying.

Why does my cron job run at the wrong time?

Almost always a timezone issue: cron uses the server's local time, not yours. Check the server's timezone with `date`, account for daylight saving transitions, and remember that server migrations silently shift every schedule. For critical jobs, use a scheduler that lets you set the timezone explicitly per job.

Does cron use AND or OR for day-of-month and day-of-week?

OR — and it's the famous exception. When both fields are restricted (neither is ``), the job runs when either matches. `0 0 1 1` runs on the 1st of the month AND every Monday. Keep one field as `*` unless you truly want both.

How do I run a cron job every 2 hours?

`0 /2 ` — at minute 0, every 2 hours. That gives you midnight, 2 AM, 4 AM, and so on. For an offset like 1 AM, 3 AM, 5 AM, use `0 1/2 *` (starting the step at hour 1).

What is the difference between cron and systemd timers?

Cron is the classic Unix scheduler: simple five-field expressions, runs on server local time, minimal logging. Systemd timers are the modern Linux alternative with calendar expressions, explicit timezone support, dependency handling, and proper logging via journald. For a quick recurring script, cron is fine; for production services on modern Linux, timers give you observability that cron never had.