Cron Expression Generator & Explainer
Describe any cron expression in plain English and preview the next runs.
| Field | Range | Example |
|---|---|---|
minute | 0–59 | */15 every 15 minutes |
hour | 0–23 | 9-17 business hours |
day of month | 1–31 | 1,15 the 1st and 15th |
month | 1–12 | 1,4,7,10 quarterly |
day of week | 0–7 (0 and 7 = Sunday) | 1-5 Mon–Fri |
Special characters
*— every possible value,— a list:1,3,5-— a range:1-5/— a step:*/10every 10 units?— no specific value (day fields, Quartz-style)
The five fields, and the one that trips everyone up
A standard cron expression is five space-separated fields:
┌───────────── minute (0–59)
│ ┌─────────── hour (0–23)
│ │ ┌───────── day of month (1–31)
│ │ │ ┌─────── month (1–12)
│ │ │ │ ┌───── day of week (0–6, Sunday = 0 or 7)
│ │ │ │ │
* * * * *
The rule that surprises nearly everyone concerns the last two fields. When
both day-of-month and day-of-week are restricted (not *),
cron treats them as a logical OR, not an AND. So:
0 0 1 * 1 # runs on the 1st of the month AND every Monday
# NOT "only when the 1st is a Monday"
If you want "the first Monday of the month", standard cron cannot express it. You
either put the condition in the script itself — [ "$(date +\%d)" -le 7 ] &&
your-job — or move to a scheduler with richer syntax, such as systemd timers
(OnCalendar=Mon *-*-1..7 03:00:00) or a job framework like Quartz or
Celery Beat.
Steps are not what people assume
*/15 in the minute field means "every 15 minutes" — 0, 15, 30, 45. It
counts from the start of the field's range, not from when the job was
installed. So */15 always fires on the quarter hour, never at
12:07, 12:22, 12:37.
Steps combine with ranges: 9-17/2 in the hour field means 9, 11, 13,
15, 17 — every two hours during business hours. And a bare number in the minute field
with * in the hour field fires once per hour, at that minute:
30 * * * * is "at half past every hour", not "every 30 minutes" (that
would be */30).
Timezone: the production incident you have not had yet
Cron runs in the system's local timezone. This causes real outages:
- DST transitions. In spring, a job scheduled at 02:30 simply does not run on the day the clock jumps from 02:00 to 03:00. In autumn it may run twice. If you need a daily job that never skips, avoid 01:00–03:00 local time.
- Cloud servers default to UTC. A job written as
0 9 * * *expecting 9 a.m. local will fire at 9 a.m. UTC, which may be 2 a.m. for your users.
Most cron implementations now accept CRON_TZ=America/New_York above the
schedule line in the crontab. Set it explicitly. Do not rely on the server default.
Production patterns worth copying
Stagger your jobs
Every cron job in the world is scheduled at 0 0 * * * (midnight). If
your job calls a third-party API, you are competing with all of them, and you will see
timeouts precisely at the top of the hour. Use an offset — 17 3 * * *
instead of 0 3 * * * — and you will avoid a whole class of flaky
failures.
Use a lock for jobs that can overlap
Cron has no memory of whether the previous run finished. If a job takes longer than
its interval, you get overlapping runs, which for a data sync or a payment job is
genuinely dangerous. Wrap it in flock:
*/5 * * * * /usr/bin/flock -n /tmp/myjob.lock /opt/scripts/myjob.sh
Log the output, or you will never debug it
By default cron mails stdout to the crontab owner — and on most servers mail is not configured, so output is silently discarded. Always redirect:
0 2 * * * /opt/scripts/backup.sh >> /var/log/backup.log 2>&1
Set PATH explicitly
Cron runs with a minimal environment (PATH=/usr/bin:/bin). Scripts that
work perfectly in your shell fail under cron because they call something in
/usr/local/bin or reference a variable from your profile. Either use
absolute paths or declare PATH= at the top of the crontab.
When cron is the wrong tool
Cron is excellent for periodic, stateless, idempotent work. It is a poor fit for jobs that need retries with backoff, dependencies between steps, distributed execution across many machines, or observability. For those, reach for a queue (Celery, Sidekiq, BullMQ), a workflow engine (Airflow, Temporal, Prefect), or systemd timers if you need better logging and calendar syntax but still want something simple.
Frequently asked questions
Almost always the day-of-month / day-of-week OR rule. When both fields are restricted, cron fires when either matches. Use * in one of them, or move the condition into the script.
It runs at 09:00 in the server's local timezone. Cloud servers usually default to UTC. Set CRON_TZ=Your/Zone in the crontab to make it explicit.
*/30 means every 30 minutes (at :00 and :30). A bare 30 means once per hour at 30 minutes past. With 30 * * * * you get one run per hour.
Jobs scheduled between roughly 01:00 and 03:00 local time may be skipped in spring or duplicated in autumn. Schedule outside that window, or run the host in UTC.
Standard cron cannot — its resolution is one minute. Use two entries, a sleep in the script, or switch to systemd timers, which support second-level scheduling.
Some implementations add a seconds field (Quartz, Spring, some Kubernetes CronJob variants) or a year field. Standard Unix cron is five fields. This tool validates the five-field form.