# Cron Expression Syntax: Fields, Examples, and Common Mistakes

By Dan Ochoa · Published 2026-10-19 · Updated 2026-10-24
Canonical: https://cronradar.com/blog/cron-expression

> Read and write cron expressions: the five fields, operators, common schedules, how to run a job every 2 months, and the time zone mistakes to avoid.

**A cron expression is five space-separated fields — minute, hour, day of month, month, and day of week — that tell cron when to run a command.** `30 2 * * 1-5` means 02:30 every Monday through Friday. `*` matches every value, `,` separates a list, `-` makes a range, and `/` steps through one. To run a job every 2 months, use `0 0 1 */2 *`: midnight on the 1st of January, March, May, July, September, and November.

The rules below follow [crontab(5)](https://man7.org/linux/man-pages/man5/crontab.5.html) from cronie, the cron daemon on Fedora, RHEL, and Arch. Debian and Ubuntu ship a Vixie-derived cron with the same five fields and operators. Kubernetes, GitHub Actions, Quartz, Spring, and AWS reuse the syntax with differences, covered near the end. For how the cron daemon finds and runs these lines, see [what is a cron job](https://cronradar.com/blog/what-is-a-cron-job).

## The Five Fields

```text
┌───────────── minute (0-59)
│ ┌───────────── hour (0-23)
│ │ ┌───────────── day of month (1-31)
│ │ │ ┌───────────── month (1-12 or JAN-DEC)
│ │ │ │ ┌───────────── day of week (0-7 or SUN-SAT; 0 and 7 are Sunday)
│ │ │ │ │
* * * * * command to run
```

| Field        | Allowed values           | Names          |
| ------------ | ------------------------ | -------------- |
| Minute       | 0-59                     |                |
| Hour         | 0-23                     |                |
| Day of month | 1-31                     |                |
| Month        | 1-12                     | `JAN` to `DEC` |
| Day of week  | 0-7 (0 and 7 are Sunday) | `SUN` to `SAT` |

Names are the first three letters of the month or day, in any case. Hours use a 24-hour clock, so 2 PM is `14`.

## Operators

| Operator | Meaning                  | Example                | Matches                           |
| -------- | ------------------------ | ---------------------- | --------------------------------- |
| `*`      | Every value              | `*` in hour            | Every hour                        |
| `,`      | List                     | `1,15` in day of month | The 1st and the 15th              |
| `-`      | Inclusive range          | `1-5` in day of week   | Monday through Friday             |
| `/`      | Step through every value | `*/15` in minute       | :00, :15, :30, :45                |
| `/`      | Step through a range     | `9-17/2` in hour       | 09:00, 11:00, 13:00, 15:00, 17:00 |

A step counts from the start of its range, not from when you saved the crontab. `*/15` in the minute field always means :00, :15, :30, and :45, whatever time it is now.

## Cron Job Expression vs. Crontab Line

A crontab line is a cron expression followed by the command to run:

```text
# minute hour day-of-month month day-of-week command
0 2 * * * /opt/app/backup.sh
```

System crontabs (`/etc/crontab` and files in `/etc/cron.d/`) add a user between the expression and the command: `0 2 * * * appuser /opt/app/backup.sh`. Mixing up the two formats is a common reason a [cron job is not running](https://cronradar.com/blog/cron-job-not-running). The [crontab guide](https://cronradar.com/blog/crontab-guide) covers editing, listing, and backing up both kinds.

In the command part, an unescaped `%` becomes a newline, and everything after the first one is sent to the command as standard input. Write `\%` instead, as in `date +\%F`, or move the command into a script.

## Common Cron Expressions

| Schedule                               | Cron expression |
| -------------------------------------- | --------------- |
| Every minute                           | `* * * * *`     |
| Every 5 minutes                        | `*/5 * * * *`   |
| Every 15 minutes                       | `*/15 * * * *`  |
| Every hour, on the hour                | `0 * * * *`     |
| Every 6 hours                          | `0 */6 * * *`   |
| Every day at midnight                  | `0 0 * * *`     |
| Every day at 02:30                     | `30 2 * * *`    |
| Weekdays at 09:00                      | `0 9 * * 1-5`   |
| Every Sunday at 03:00                  | `0 3 * * 0`     |
| 1st of every month at midnight         | `0 0 1 * *`     |
| 1st and 15th of every month at 06:00   | `0 6 1,15 * *`  |
| Every 2 months, on the 1st at midnight | `0 0 1 */2 *`   |
| Every quarter (Jan, Apr, Jul, Oct)     | `0 0 1 */3 *`   |
| Once a year, January 1 at midnight     | `0 0 1 1 *`     |

## How to Run a Job Every 2 Months

Put a step in the month field:

```text
0 0 1 */2 * /opt/app/bimonthly-report.sh
```

That runs at 00:00 on the 1st of January, March, May, July, September, and November. For the even months, start the range at February: `0 0 1 2-12/2 *` runs in February, April, June, August, October, and December.

A step restarts at the end of its range, so it only gives an even interval when it divides the range. Every 2, 3, 4, or 6 months works because each divides 12. `*/5` in the month field means January, June, and November, then January again two months later. The minute field behaves the same way: `*/7` runs at :56 and then at :00, four minutes apart.

Cron has no memory of the last run, so it can't express "every other week" or "every 10 days" either. Schedule the job more often and let a small guard decide whether this run does the work:

```bash
#!/usr/bin/env bash
# Cron runs this daily; the work happens at most once every 14 days.
set -euo pipefail
stamp=/var/lib/myapp/last-run
if [ -f "$stamp" ] && [ $(( $(date +%s) - $(stat -c %Y "$stamp") )) -lt $(( 14 * 86400 - 3600 )) ]; then
  exit 0
fi
/opt/app/fortnightly-task.sh
touch "$stamp"
```

The one-hour allowance stops a run that took a few minutes from pushing the next one a day later. Because the stamp is written only after the task succeeds, a failed run is retried the next day.

A job that runs six times a year is the easiest kind to lose. If it stops, nothing complains until someone needs what it was supposed to produce, possibly two months later. Give infrequent jobs a deadline; the last section shows how.

## Day of Month and Day of Week Together

When both day fields are restricted, cronie runs the command when **either** one matches. `0 9 1 * MON` runs at 09:00 on the 1st of every month _and_ every Monday, not only on Mondays that fall on the 1st.

For "the first Monday of the month", restrict the day of month to 1-7 and check the weekday in the command:

```text
0 9 1-7 * * [ "$(date +\%u)" = 1 ] && /opt/app/monthly-review.sh
```

[Cron last day of month](https://cronradar.com/blog/cron-last-day-of-month) covers the same date test for the last day of the month and the last weekday.

Not every scheduler uses the either-field rule. Quartz and AWS EventBridge make you put `?` in one of the two fields, and CronRadar requires a date to match both. See [CronRadar's schedule syntax](https://docs.cronradar.com/schedules) before monitoring a schedule that restricts both.

## Shortcuts: @daily, @reboot, and Others

cronie accepts these in place of the five fields:

| Shortcut                 | Equivalent       |
| ------------------------ | ---------------- |
| `@yearly` or `@annually` | `0 0 1 1 *`      |
| `@monthly`               | `0 0 1 * *`      |
| `@weekly`                | `0 0 * * 0`      |
| `@daily`                 | `0 0 * * *`      |
| `@hourly`                | `0 * * * *`      |
| `@reboot`                | Once, at startup |

Kubernetes CronJobs accept the same shortcuts except `@reboot`. Many libraries and services accept none of them, so write the five fields when you aren't sure. [@reboot in cron](https://cronradar.com/blog/cron-reboot) covers when an `@reboot` line runs and why it can fail at boot.

## Time Zones and Daylight Saving Time

Cron uses the time zone of the cron daemon, normally the system zone in `/etc/localtime`. cronie also lets a crontab set `CRON_TZ` for the entries below it; Debian's cron and BusyBox crond have no equivalent. On a server set to UTC, `0 9 * * *` runs at 09:00 UTC, which is 04:00 or 05:00 in New York depending on daylight saving time. [Cron timezone](https://cronradar.com/blog/cron-timezone) shows how to check the daemon's zone and run one job in another.

Daylight saving affects only jobs in the hour the clocks skip or repeat, and the implementations disagree. For entries in the daemon's own zone, cronie and Debian's cron run a fixed-time job from the skipped hour right after the jump and run one in the repeated hour once, as their cron(8) pages say. cronie's crontab(5) says instead that skipped times never match and repeated times run twice. In the 1.7.2 source, the catch-up logic covers only the daemon's zone ([cron.c](https://github.com/cronie-crond/cronie/blob/cronie-1.7.2/src/cron.c#L398-L479)), so crontab(5) describes `CRON_TZ` entries ([cron.c](https://github.com/cronie-crond/cronie/blob/cronie-1.7.2/src/cron.c#L563-L598)), and BusyBox crond treats every entry that way. Interval schedules such as `*/15 * * * *` run through both passes of the repeated hour. [Cron daylight saving time](https://cronradar.com/blog/cron-daylight-saving-time) has the full table. Keep important jobs out of the 01:00-03:00 local window, or run the server on UTC.

Hosted schedulers set their own rules:

- **GitHub Actions** runs `schedule` workflows in UTC unless you give an IANA `timezone`. The shortest interval is every 5 minutes, and runs can be delayed under load. See [GitHub's schedule event docs](https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#schedule).
- **Kubernetes CronJobs** use the kube-controller-manager's local time zone unless `.spec.timeZone` is set. `CRON_TZ` or `TZ` inside the schedule is rejected. See [Kubernetes CronJob time zones](https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/#time-zones).
- **CronRadar** evaluates monitor schedules in UTC, so write the expression you send it in UTC.

## Five, Six, or Seven Fields

| Scheduler                                                                                                                                              | Fields                               | Differences from Linux cron                                                   |
| ------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------ | ----------------------------------------------------------------------------- |
| Linux cron (cronie)                                                                                                                                    | 5                                    | Baseline                                                                      |
| Kubernetes CronJob                                                                                                                                     | 5                                    | Day of week 0-6; `?` means `*`; `.spec.timeZone`                              |
| GitHub Actions                                                                                                                                         | 5                                    | UTC by default; every 5 minutes at most                                       |
| [Spring `CronExpression`](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/scheduling/support/CronExpression.html) | 6, seconds first                     | Adds `L`, `W`, `#`, `?`                                                       |
| [Quartz](https://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html)                                                       | 6 or 7, seconds first, optional year | Day of week 1-7 with Sunday = 1; `?` required in one day field; `L`, `W`, `#` |
| [AWS EventBridge Scheduler](https://docs.aws.amazon.com/scheduler/latest/UserGuide/schedule-types.html)                                                | 6, year last, inside `cron(...)`     | `?` required in one day field; `L`, `W`, `#`                                  |
| [CronRadar](https://docs.cronradar.com/schedules)                                                                                                      | 5                                    | No shortcuts, `L`, `W`, `#`, or `?`; both day fields must match; UTC          |

Cron's `0 2 * * *` is `0 0 2 * * ?` in Quartz. Copying an expression between systems without converting it gets you a schedule that is rejected or, worse, accepted with a different meaning: `1` in the day-of-week field is Monday in cron and Sunday in Quartz. [Spring @Scheduled cron jobs](https://cronradar.com/blog/spring-scheduled-monitoring) covers Spring's six-field form.

## Check an Expression Before You Deploy It

Paste it into [crontab.space](https://crontab.space/), a free cron expression generator and validator, to read it back in plain English and preview the next run times. After installing it, run `crontab -l` as the user that owns the job to confirm cron received the line you meant. If `crontab` refuses the line with `bad minute`, `bad command`, or another message, [crontab error messages](https://cronradar.com/blog/crontab-errors) explains what each one means.

## Know When the Schedule Doesn't Run

A valid expression only proves cron will try. It doesn't show that the job started, finished, or did its work: the server can be off, a deploy can overwrite the crontab, or the script can fail on its first line. Cron reports none of this.

[How to know if a cron job ran](https://cronradar.com/blog/how-to-know-if-cron-job-ran) compares the ways to check. The one that catches a job that never started is a [dead man's switch](https://cronradar.com/blog/cron-dead-mans-switch): the job checks in after each run, and a monitor that knows the schedule alerts when a check-in is late. With CronRadar, a check-in can carry the schedule, and the first one creates the monitor. Put the check-in in a small wrapper script so the encoded `%` signs stay out of the crontab:

```bash
#!/usr/bin/env bash
# /opt/app/bimonthly-report-monitored.sh: run the report, then report its success.
/opt/app/bimonthly-report.sh || exit $?
curl --fail --silent --show-error --max-time 5 \
  --user "$CRONRADAR_API_KEY:" \
  "https://cron.life/api/v1/ping/bimonthly-report?schedule=0%200%201%20*/2%20*" || true
```

Set `CRONRADAR_API_KEY` in the job's environment and schedule the wrapper instead of the report:

```text
0 0 1 */2 * /opt/app/bimonthly-report-monitored.sh
```

A failed report exits with its own code and sends nothing, and an unreachable CronRadar never fails the job. Because every check-in carries the schedule, the monitor recreates itself if it is ever deleted.

If January's report doesn't arrive, you hear about it in January, not in March.

## Frequently Asked Questions

### What is a cron expression?

Five fields — minute, hour, day of month, month, and day of week — that describe when a scheduled job runs. In a crontab, the command to run follows the expression on the same line.

### Does cron support seconds?

No. Standard cron's smallest unit is one minute. Spring and Quartz add a seconds field at the start, which is why their expressions have six or seven fields. Work that must run more often than once a minute usually belongs in a long-running worker; [cron every 30 seconds](https://cronradar.com/blog/cron-every-30-seconds) compares that with the workarounds.

### What does `*/5` mean in a cron expression?

Every fifth value of the field, counted from the start of its range. In the minute field, `*/5` runs at :00, :05, :10, and so on through :55.

### How do I run a cron job every 2 months?

Use `0 0 1 */2 *` for midnight on the 1st of January, March, May, July, September, and November, or `0 0 1 2-12/2 *` for the even months.

### Can cron run a job every other week?

Not with the expression alone, because cron doesn't remember the last run. Run the job weekly or daily and have it check how long it has been since its last successful run, as in the guard script above.
