ByteKit
Home › Blog › Cron Expressions

Cron Expressions Explained: From Five Fields to Real Schedules

Cron looks trivial — five fields, a few symbols — and then a job fires at the wrong hour, twice in one night, or never at all. The syntax is the easy part; the traps live in the day-field semantics, the @ shortcuts, and the timezone the daemon runs in. This article walks the five fields, the special strings, and the DST traps, and ends with a table of schedules you will actually write. To test an expression before it touches a server, use the cron expression generator.

The five fields, one by one

Standard cron has five fields, in order: minute (0-59), hour (0-23), day of month (1-31), month (1-12), and day of week (0-7, where both 0 and 7 mean Sunday). Three operators cover almost everything: a comma lists values (1,15), a hyphen defines a range (1-5), and a slash sets a step — */15 in the minute field means every fifteen minutes.

The first genuine trap is the relationship between day-of-month and day-of-week. In Vixie cron, used by most Linux distributions, the two fields are OR-ed when both are restricted: 0 0 1 * 1 fires on the first of the month and every Monday, not only Mondays that fall on the first. Expressing AND requires a date check inside the job itself. Other engines differ — Quartz uses ? for "no specific value" in one day field, and systemd timers use OnCalendar syntax — so know which engine will read your string.

Special strings: @daily, @hourly, @reboot

The @ shortcuts replace the five fields in crontab files: @reboot runs once at daemon start, @yearly and @annually are 0 0 1 1 *, @monthly is 0 0 1 * *, @weekly is 0 0 * * 0, @daily and @midnight are 0 0 * * *, and @hourly is 0 * * * *.

Two deserve comment. @reboot is an event trigger, not a schedule — on a long-lived server it runs once and then never again, which surprises people who expected a timer. And @daily means midnight in the daemon's local timezone, which leads into the next section. @ strings are a crontab-level convenience: formats that take a raw expression, such as a Kubernetes CronJob spec, want the expanded five-field form.

Timezone traps and DST

Cron evaluates expressions in the daemon's local timezone, and that detail has caused more production incidents than the syntax ever has. A job scheduled at 02:30 in a timezone that springs forward never runs that day — the clock skips from 02:00 to 03:00 and the 02:30 slot simply does not exist. On fall-back night the same job may run twice, depending on how the implementation handles the repeated hour.

Containers make this worse: a Docker container defaults to UTC unless TZ is set, so a job that fires at 09:00 on your laptop runs at a different wall-clock hour in the cluster. The fixes are unglamorous but reliable: set the timezone explicitly in the crontab or container spec, schedule critical jobs away from DST boundaries (avoid 01:00-03:00 local), or run the daemon in UTC and convert with the timezone converter. Cross-check a sample run against the timestamp converter to confirm exactly when an expression fires.

Ten schedules you will actually need

ScheduleExpression
Every minute* * * * *
Every 15 minutes*/15 * * * *
Top of every hour0 * * * *
Daily at 02:000 2 * * *
Weekdays at 08:3030 8 * * 1-5
Every Monday at 06:000 6 * * 1
First day of month, 00:1515 0 1 * *
Business hours, hourly0 9-17 * * 1-5
Every 6 hours0 */6 * * *
Sundays at 03:00 (backup)0 3 * * 0

Read every expression above with the day-field OR rule in mind, and prefer step values that divide the hour evenly — */7 in the minute field fires at 0, 7, 14, drifting against the clock. When you inherit someone else's crontab, translate the intent back to English: 30 4 * * 1 says Monday 04:30, not "early morning."

Try it yourself

Cron Expression Generator — paste or build a five-field expression and see the next run times explained in plain English.

Related tools: Timestamp Converter · Timezone Converter

← All posts