Cron Expression Generator & Explainer

Describe any cron expression in plain English and preview the next runs.

Cron expression

Field reference
FieldRangeExample
minute0–59*/15 every 15 minutes
hour0–239-17 business hours
day of month1–311,15 the 1st and 15th
month1–121,4,7,10 quarterly
day of week0–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: */10 every 10 units
  • ? — no specific value (day fields, Quartz-style)
Next 5 run times

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.