Cron Expressions: Read the Syntax and Calculate the Next Runs

Cron Expressions: Read the Syntax and Calculate the Next Runs

Read a cron expression field by field, check its next runs in UTC or local time and avoid traps with steps, time zones and dialects, all in your browser.

16.10.2026
18 min read
Share this article:
CronSchedulingTime zoneDevOpsTutorial

A cron expression is read field by field and checked against dates

A line like */15 9-17 * * 1-5 drives a backup, a CI workflow or a Kubernetes CronJob. It is short, and few people reread it. Misread one field and the job runs at the wrong hour, twice, or never. The cron parser takes five fields (minute, hour, day of month, month, day of week) or six, with seconds first. It also takes aliases such as @daily and @hourly. It shows a sentence that sums up the schedule and the value of each field. It also lists the next 5, 10 or 25 runs, in UTC or in your browser's time zone. The calculation happens in your tab and the expression is sent to no one. This guide covers the syntax and the aliases, then shows how to read the result. Next come the traps that cost the most. A step does not line up, two day fields combine as an OR, a date does not exist, and the time zone and the clock change of 25 October 2026 move everything. It then compares the dialects of crontab, Kubernetes, GitHub Actions and AWS EventBridge, and redoes the calculations in code. Related tools are listed in the developer utilities hub.

Five standard fields, or six with seconds first. Minute 0 to 59, hour 0 to 23, day of month 1 to 31, month 1 to 12 or JAN to DEC, day of week 0 to 7 or SUN to SAT (0 and 7 both mean Sunday)
Lists (<code>1,15</code>), ranges (<code>9-17</code>) and steps (<code>*/15</code>, <code>10-50/10</code>). Month and day names work in upper or lower case
The aliases <code>@hourly</code>, <code>@daily</code>, <code>@midnight</code>, <code>@weekly</code>, <code>@monthly</code>, <code>@yearly</code> and <code>@annually</code>, replaced by their five-field form in the input box. <code>@reboot</code> is rejected
A summary sentence for the usual schedules (every 15 minutes, every day at 09:00, Monday to Friday), and the field-by-field detail for the rest
The next 5, 10 or 25 runs, with the time until the first one, in UTC or in the browser's local time
A note when day of month and day of week are both set: either one triggers the run
No upload: the calculation stays in the browser

What the parser is for, and where it stops

Good uses

The parser answers one question: at which moments does this expression fire? Use it to check that before you deploy.

Reread a colleague’s line or an old crontab before copying it: the summary sentence first, the five fields next
Check a step. <code>*/7</code> does not repeat the job every 7 minutes (see below), and the next runs show it
Check a month end such as <code>0 0 31 * *</code>, which skips the 30-day months. Ask for 25 runs to see the pattern
Read the schedule in your own time zone by switching the display to Local, knowing the calculation stays in UTC (see the mistakes below)
Write an alias in its five-field form before pasting it into a tool that accepts no macros, such as GitHub Actions
Copy the description or the list of runs into a ticket or a code review, so the reviewer sees the dates as well as the line
What belongs elsewhere

The parser calculates dates. The rest of the chain is checked somewhere else.

Turn the epoch of a log line or a token into a readable date: the <a href="/en/unix-timestamp" class="text-primary hover:underline">Unix timestamp converter</a>
Check that a Kubernetes manifest is well formed: the <a href="/en/validate-kubernetes" class="text-primary hover:underline">Kubernetes validator</a>, covered in the <a href="/en/blog/kubernetes-validate-yaml-manifests-online" class="text-primary hover:underline">Kubernetes guide</a>
Check that a GitHub Actions workflow is well formed: the <a href="/en/validate-github-actions" class="text-primary hover:underline">GitHub Actions validator</a>, covered in the <a href="/en/blog/github-actions-workflow-yaml-lint-guide" class="text-primary hover:underline">GitHub Actions guide</a>. The <a href="/en/blog/gitlab-ci-format-validate-yaml-guide" class="text-primary hover:underline">GitLab CI guide</a> does the same for <code>.gitlab-ci.yml</code>
Test the job itself: the parser starts nothing, reads no log and does not know whether the machine was on
Monitor the real run: a correct schedule does not prove the job ran. You need logs, an alert or a last-success check
Handle dialect extras: the parser knows none of <code>?</code>, <code>L</code>, <code>W</code> or <code>#</code>, and it has no year field
Schedule a one-off run: cron repeats, it cannot say "once". Use <code>at</code> for that

Reading the results without mistakes

The result, field by field

Enter the expression in the cron parser. The dates below start from Friday 16 October 2026 at 12:00 UTC. The tool starts from the present moment, so your dates will be different.

<code>0 9 * * 1-5</code> gives the sentence "At 09:00 on weekdays (Mon–Fri)". The next runs are Monday 19, Tuesday 20, Wednesday 21, Thursday 22 and Friday 23 October at 09:00 UTC
<code>*/15 9-17 * * 1-5</code> has no exact sentence. The tool writes "Schedule breakdown", adds a note, then lists the five fields and the runs: Friday 16 at 12:15, 12:30, 12:45, 13:00 and 13:15
The last start of the day for <code>9-17</code> with <code>*/15</code> is 17:45. The hour range includes the whole of hour 17, up to 17:59
The five boxes repeat each field as you wrote it. Only aliases are expanded: <code>@daily</code> becomes <code>0 0 * * *</code> in the input box
Day of week accepts 0 and 7 for Sunday. <code>0 0 * * 7</code> and <code>0 0 * * 0</code> give the same dates
The "Next" line gives the time until the first run, which is enough to spot an expression that only fires in six months
A step is not counted from the last run

A step is calculated inside the range of the field. At the start of each range the counter goes back to zero.

<code>*/7 * * * *</code> fires at minutes 0, 7, 14 and so on up to 56, then returns to minute 0: 12:49, 12:56, then 13:00, which is 4 minutes later. The crontab manual page gives the same case with <code>*/35</code>
<code>0 */5 * * *</code> fires at hours 0, 5, 10, 15 and 20. After 20:00 comes 00:00, so the gap is 4 hours, not 5
<code>0 0 */2 * *</code> targets days 1, 3, 5 up to 31. The 31st of October is followed by the 1st of November, two days in a row
For a true "every N units", pick an N that divides 60 for minutes or 24 for hours, or count in the script from the time of the last success
A step on a single value, such as <code>20/15</code>, starts at that value and runs to the maximum of the field: minutes 20, 35 and 50
When day of month and day of week combine

When both fields are set, cron does not combine them with AND. It runs the job when either one matches, according to the crontab manual page. The tool flags it with a note under the summary sentence.

<code>0 0 1 * 1</code> fires on the 1st of the month and on every Monday. The next runs are Monday 19 and 26 October, Sunday 1 November, then Monday 2 and 9 November
The first Monday of the month does not exist in standard cron. Writing <code>1-7</code> for the day of month and <code>1</code> for Monday gives an either-or, so too many runs
Workaround: schedule every Monday and test the date in the command, with <code>[ "$(date +\%d)" -le 7 ]</code>. In a crontab, the % sign is written <code>\%</code>
When one of the two fields is <code>*</code>, the other decides alone: <code>0 9 * * 1-5</code> has no OR
Dates missing from the calendar

A valid expression can fail to fire at all. The tool searches about two years ahead and tells you.

<code>0 0 30 2 *</code> aims at a 30 February: no run. The tool shows "No upcoming runs found in the search window"
<code>0 0 31 * *</code> only fires in the 31-day months. From 16 October 2026: 31 October and 31 December 2026, then 31 January, 31 March, 31 May, 31 July and 31 August 2027
<code>0 0 29 2 *</code> has one date in the window, 29 February 2028. The tool shows "Only partial results before the search limit", which is normal: the next one falls in 2032
The last day of the month has no spelling in standard cron. Schedule the 28th to the 31st and test tomorrow's date in the command, for example with <code>[ "$(date -d tomorrow +\%d)" = 01 ]</code> (with GNU <code>date</code>)

Four mistakes you only see in production

Thinking the Local display changes the time zone of the schedule

The parser always calculates in UTC. The Local option only changes how the dates it found are written. 0 9 * * * gives 09:00 UTC, which the tool writes as 11:00 in a browser set to Paris on 16 October 2026. A cron started on a server set to Paris fires at 09:00 Paris time, which is 07:00 UTC until 24 October and 08:00 UTC after that. Which zone applies depends on the system that runs the job. The cron daemon follows the time zone of the machine. A Kubernetes CronJob follows the one of the kube-controller-manager, unless .spec.timeZone is set. GitHub Actions counts in UTC by default.

Reading the time shown in Local as the time the job will run on the server
Forgetting that the server, the container and your laptop may not share a time zone
Writing a local time in a system that counts in UTC
Putting <code>CRON_TZ</code> or <code>TZ</code> in the <code>schedule</code> field of a CronJob: Kubernetes rejects the resource, because the zone belongs to <code>.spec.timeZone</code> and not to that field
Decide the time zone before you write the hour. To check a schedule expressed in local time, convert the hour to UTC by hand and read the dates in UTC.
Letting a fixed hour cross a clock change

Europe goes back to winter time on Sunday 25 October 2026. At 03:00 on the old clock, it is 02:00 again. The United States changes a week later, on 1 November. A job set to 30 2 * * * then meets an hour that exists twice when the clocks go back, or not at all when they go forward. Implementations disagree. The Debian manual page for cron(8) says jobs in the skipped hour run soon after the change, and jobs in the repeated hour are not run again. The crontab(5) page published on man7.org says the opposite. A time that does not exist never matches, and a repeated time fires twice. GitHub Actions, with the timezone key, moves a run that falls in the skipped hour to the next valid time.

Putting an important job between 01:00 and 04:00 local time
Assuming a daily job runs exactly 24 hours after the previous one
Counting on a single start when the hour can be played twice or not at all
Writing a job that cannot stand being started twice
Schedule in UTC when you can, otherwise at an hour that always exists, such as 05:00, and write jobs that are safe to run again.
Pasting an expression from another dialect

The same schedule is written differently depending on the tool. AWS EventBridge adds a year field and requires a ? in one of the two fields that hold a day. Quartz and Spring put seconds first. The parser rejects what it does not know and names the field and the token at fault, for example "Invalid value in Day of month (?)".

Pasting EventBridge's <code>0 0 ? * MON-FRI *</code>: six fields while the setting is on five, and a <code>?</code> the tool cannot read. <code>L</code>, <code>W</code> and <code>#</code> are rejected the same way
Writing a range that crosses midnight, such as <code>22-2</code>: the start is past the end and the tool rejects it. Write <code>22-23,0-2</code>
Switching to six fields, entering non-zero seconds, then switching back to five: the switch back is refused until the seconds are 0
Typing an alias that is not on the list, such as <code>@reboot</code> or <code>@every5m</code>, or using one in GitHub Actions, which accepts none
Note the dialect of the target before you copy an expression, then convert days, year and seconds by hand. Keep the five-field form in the parser.
Validating the schedule and forgetting the run

A correct expression guarantees neither that the job starts, nor that it starts once. The Kubernetes documentation warns that Job creation is approximate: two Jobs may be created, or none, so jobs must be idempotent. The default concurrency policy is Allow, which lets a run start while the previous one is still going. If the controller counts more than 100 missed starts, it stops starting the job and logs an error. GitHub Actions delays scheduled runs at times of high load, especially at the start of every hour, and may drop some. In a public repository, scheduled workflows are disabled after 60 days without activity.

Scheduling <code>0 * * * *</code> on GitHub Actions, where the start of the hour is the busiest moment
Leaving <code>concurrencyPolicy: Allow</code> on a job that takes longer than its interval
Not setting <code>startingDeadlineSeconds</code> and finding, after a controller outage, more than 100 missed starts and no run at all
Relying on a scheduled workflow in a public repository that no longer gets commits
Pick an offset minute such as <code>17 * * * *</code>, set <code>concurrencyPolicy: Forbid</code> when two runs must not overlap, and add an alert on the last success.

Fields, aliases and dialects at a glance

The five fields

Every field takes the same operators. Only the bounds change.

Minute: 0 to 59
Hour: 0 to 23
Day of month: 1 to 31
Month: 1 to 12, or JAN to DEC
Day of week: 0 to 7 (0 and 7 both mean Sunday), or SUN to SAT
<code>*</code> means every value, <code>,</code> separates a list, <code>-</code> draws an inclusive range (<code>8-11</code> gives 8, 9, 10 and 11) and <code>/</code> sets a step
With six fields, the seconds (0 to 59) come first
The aliases

An alias replaces the whole expression. The parser expands them in the input box.

<code>@hourly</code> equals <code>0 * * * *</code>
<code>@daily</code> and <code>@midnight</code> equal <code>0 0 * * *</code>
<code>@weekly</code> equals <code>0 0 * * 0</code>, Sunday at midnight
<code>@monthly</code> equals <code>0 0 1 * *</code>
<code>@yearly</code> and <code>@annually</code> equal <code>0 0 1 1 *</code>
<code>@reboot</code> depends on the machine starting and has no date: the parser rejects it
GitHub Actions accepts none of these aliases
The same schedule in five dialects

Before you copy an expression, check what the target understands.

Unix crontab: five fields, in the time zone of the system. Some implementations also read a <code>CRON_TZ</code> variable
Kubernetes CronJob: five fields, <code>?</code> equals <code>*</code>, aliases accepted. The zone is set with <code>.spec.timeZone</code> (stable since Kubernetes 1.27), otherwise it is the one of the kube-controller-manager
GitHub Actions: five fields in POSIX syntax, no aliases, UTC by default, an optional timezone key described in the documentation, a minimum interval of 5 minutes, and runs from the default branch only
AWS EventBridge: six fields, <code>cron(minute hour day-of-month month day-of-week year)</code>, with a <code>?</code> required in one of the two fields that hold a day. The <code>L</code>, <code>W</code> and <code>#</code> operators are allowed. Scheduled rules count in UTC
Quartz and Spring: six fields, seconds first
The FastMinify parser: five or six fields (seconds first), aliases, month and day names, and nothing else

The cron parser, step by step

Reading an expression

The cron parser analyses as you type, after a short pause. The Parse button, or the Enter key in the field, runs the calculation without waiting.

1

Type or pick a preset

Six presets fill the field: every 5 minutes, every 15 minutes, every hour, every day at midnight, weekdays at 09:00 and the 1st of every month. An alias such as @daily is replaced by its five-field form.

2

Read the sentence

For simple schedules, a sentence sums up the expression: every minute, every N minutes, every hour, every day at a given time, Monday to Friday, day N of every month, once a year. If not, the tool shows "Schedule breakdown" and leaves you to read the fields.

3

Check the boxes

Each box carries the name of a field and its value. A note appears when day of month and day of week are both set, as a reminder that either one is enough.

4

Read the next runs

The list gives the full dates, with a "Next" line that gives the delay before the first one. A warning appears if no date exists or if the search stops before it finds enough.

Setting the options

Three settings under the input field change what the tool calculates or shows.

1

Five or six fields

Five fields is the default. Switching to six adds 0 at the start of the expression, so seconds at zero. Switching back to five removes that zero, and the tool refuses while the seconds are anything other than 0.

2

Runs to show

Choose 5, 10 or 25. Take 25 to see the pattern of a step, a month end or two day fields combined.

3

Time zone

UTC is the default. Local uses the time zone of your browser. A badge above the sentence reminds you of the choice. The calculation stays in UTC either way: only the way the dates are written changes.

4

Copy

Copy description takes the sentence, Copy all takes the list, and each line has its own Copy button.

Understanding a rejection

When the expression is rejected, the tool says why and calculates nothing.

1

Empty expression

A field emptied after typing shows "Enter a cron expression".

2

Wrong number of fields

"Invalid cron expression for the selected field count" points to four fields, or six while the setting is on five. Change the setting or fix the tokens.

3

Invalid value

A message such as "Invalid value in Hour (22-2)" names the field and the token at fault, and the box of that field is highlighted.

4

Unsupported alias

@reboot and any alias outside the list give a message that recalls the ones the tool accepts.

The same schedules in a crontab, Kubernetes, GitHub Actions and Node

A crontab line

The line goes in crontab -e. The command takes the rest of the line: an unprotected % becomes a newline there, hence the \% in the example (see the crontab manual page). Cron does not run your login profile, so give the full path of the programs.

Basic example

# m h dom mon dow command */15 9-17 * * 1-5 /usr/local/bin/sync.sh >> /var/log/sync.log 2>&1 0 2 * * * /usr/bin/tar -czf /backup/site-$(date +\%F).tar.gz /var/www # First Monday only: cron ORs day-of-month and day-of-week, so test the date in the command 0 9 * * 1 [ "$(date +\%d)" -le 7 ] && /usr/local/bin/monthly-report.sh
A Kubernetes CronJob

The time zone is declared in .spec.timeZone, never in schedule. Check the manifest with the Kubernetes validator before you apply it.

Basic example

apiVersion: batch/v1 kind: CronJob metadata: name: nightly-report spec: schedule: "0 9 * * 1-5" # five fields, no CRON_TZ or TZ prefix timeZone: "Europe/Paris" # without it, the time zone of kube-controller-manager applies concurrencyPolicy: Forbid # skip a run while the previous one is still running startingDeadlineSeconds: 300 # do not start a run more than 5 minutes late jobTemplate: spec: template: spec: restartPolicy: OnFailure containers: - name: report image: registry.example.com/report:1.4.2 command: ["/app/run-report"]
A GitHub Actions workflow

The workflow only fires if it exists on the default branch. The GitHub Actions validator checks the file, and the cron parser checks the dates.

Basic example

on: schedule: # Minute 17 avoids the busiest moment, the start of the hour. UTC by default. - cron: "17 7 * * 1-5" # Optional IANA time zone, as described in the GitHub documentation - cron: "30 5 * * 1-5" timezone: "America/New_York"
The cron-parser library with Node

To calculate the next runs in a time zone, as a real cron does, pass the tz option. The example uses version 5 of the library. It shows the gap with the FastMinify parser, which calculates in UTC: the same line gives 07:00 UTC before 25 October 2026 and 08:00 UTC after it, for a Paris hour.

Basic example

import { CronExpressionParser } from 'cron-parser' // Same line, two zones: 09:00 UTC is not 09:00 in Paris const options = { currentDate: '2026-10-23T12:00:00Z', tz: 'Europe/Paris' } const paris = CronExpressionParser.parse('0 9 * * *', options) for (let i = 0; i < 3; i++) console.log(paris.next().toISOString()) // 2026-10-24T07:00:00.000Z (09:00 CEST, UTC+2) // 2026-10-25T08:00:00.000Z (09:00 CET, UTC+1: the clocks went back that night) // 2026-10-26T08:00:00.000Z const utc = CronExpressionParser.parse('0 9 * * *', { ...options, tz: 'UTC' }) console.log(utc.next().toISOString()) // 2026-10-24T09:00:00.000Z, what the parser tool shows

Conclusion

A cron expression fits in five fields, and almost every error comes from what you cannot see in it. A step restarts from zero, two day fields combine as an OR, a date does not exist, and a time zone or a clock change moves everything. The cron parser puts dates next to the line, so you can spot these cases before deployment. It calculates in UTC, starts no job and only knows the five or six fields and the common aliases. The target matters as much as the expression, because crontab, Kubernetes, GitHub Actions, EventBridge and Quartz do not read the same syntax. Keep jobs that are safe to run again and watch their last success. The developer utilities hub lists the related tools.

Paste the expression into the parser and read the dates as well as the summary sentence
Ask for 25 runs to see the pattern of a step, a month end or two day fields combined
Decide the time zone before you write the hour, and write jobs that are safe to run again
Avoid the night of the clock change and the start of the hour for a job that matters
Note the dialect of the target: aliases, seconds, year and time zone are not handled the same way everywhere
Share this article
Share this article: