TabOnly

Cron Expression Simulator

What a cron expression means, the next runs in any timezone, and what it does when the clocks change.

cron expressionCtrl+Shift+L

reading…

runs
next runs · UTC25 runs
localoffsetgaputc
2026-09-24 09:00:00UTC2026-09-24T09:00:00Z
2026-09-25 09:00:00UTC+1d2026-09-25T09:00:00Z
2026-09-28 09:00:00UTC+3d2026-09-28T09:00:00Z
2026-09-29 09:00:00UTC+1d2026-09-29T09:00:00Z
2026-09-30 09:00:00UTC+1d2026-09-30T09:00:00Z
2026-10-01 09:00:00UTC+1d2026-10-01T09:00:00Z
2026-10-02 09:00:00UTC+1d2026-10-02T09:00:00Z
2026-10-05 09:00:00UTC+3d2026-10-05T09:00:00Z
2026-10-06 09:00:00UTC+1d2026-10-06T09:00:00Z
2026-10-07 09:00:00UTC+1d2026-10-07T09:00:00Z
2026-10-08 09:00:00UTC+1d2026-10-08T09:00:00Z
2026-10-09 09:00:00UTC+1d2026-10-09T09:00:00Z
2026-10-12 09:00:00UTC+3d2026-10-12T09:00:00Z
2026-10-13 09:00:00UTC+1d2026-10-13T09:00:00Z
2026-10-14 09:00:00UTC+1d2026-10-14T09:00:00Z
2026-10-15 09:00:00UTC+1d2026-10-15T09:00:00Z
2026-10-16 09:00:00UTC+1d2026-10-16T09:00:00Z
2026-10-19 09:00:00UTC+3d2026-10-19T09:00:00Z
2026-10-20 09:00:00UTC+1d2026-10-20T09:00:00Z
2026-10-21 09:00:00UTC+1d2026-10-21T09:00:00Z
2026-10-22 09:00:00UTC+1d2026-10-22T09:00:00Z
2026-10-23 09:00:00UTC+1d2026-10-23T09:00:00Z
2026-10-26 09:00:00UTC+3d2026-10-26T09:00:00Z
2026-10-27 09:00:00UTC+1d2026-10-27T09:00:00Z
2026-10-28 09:00:00UTC+1d2026-10-28T09:00:00Z
try

The two day fields are OR, and the library that describes them says “and”

Put a value in both the day-of-month field and the day-of-week field and cron runs the job when either matches — not both. It is the one rule in this format that reverses the answer rather than shifting it, and it is documented in a single sentence of man 5 crontab that nobody has read:

“If both fields are restricted (ie, are not *), the command will be run when either field matches the current time. For example, 30 4 1,15 * 5 would cause a command to be run at 4:30 am on the 1st and 15th of each month, plus every Friday.”

So 0 0 13 * 5 is not midnight on Friday the 13th. Starting 1 April 2026 — a month whose 13th is a Monday, so the natural reading predicts no runs at all — it fires five times:

RunWhy it matched
Fri 3 Aprilday of week
Fri 10 Aprilday of week
Mon 13 Aprilday of month
Fri 17 Aprilday of week
Fri 24 Aprilday of week

The reason this is worth a page rather than a footnote is that the library this page writes its own descriptions with reads it as an AND. cronstrue 3.24.0 describes 0 0 13 * 5 as “At 12:00 AM, on day 13 of the month, and on Friday” — and gives the manual’s own example, 30 4 1,15 * 5, the same treatment. “And” is the natural-language reading of a conjunction, and the behaviour is a disjunction. This page uses cronstrue too, and overrides it in exactly this case: when both day fields are restricted, the sentence above the table is written here, with the word or in it.

What happens on the two days a year the clocks move

A cron expression is a pattern over a wall clock, and twice a year a wall clock stops being a reliable index of time. On the spring transition some readings do not exist; on the autumn one some happen twice. What a scheduler does about that is a decision, not arithmetic, and the honest thing a simulator can do is show the transition rather than quietly pick a side.

The behaviour modelled here is the one cronie’s crontab(5) documents: non-existent times “will never match, causing jobs scheduled during the missing times not to be run”, and times that occur more than once “will cause matching jobs to be run twice”. In America/New_York, that is:

ExpressionSun 8 March 2026 — 02:00 does not exist
30 2 * * *does not run at all; the next run is 02:30 on the 9th
*/15 2 * * *all four runs skipped — 02:00, 02:15, 02:30, 02:45
0 * * * *01:00, then 03:00 — the 02:00 run never happens, so the day has 23 of them
ExpressionSun 1 November 2026 — 01:00 happens twice
0 * * * *runs twice at 01:00 — once at 05:00Z, once at 06:00Z
30 1 * * *runs twice at 01:30 — 05:30Z and 06:30Z

Those rows are what the table at the top of this page renders, marked inline, for any expression and any of the IANA zones your browser knows. They are also where this page and the JavaScript cron library it is tested against part company on purpose: cron-parser 5.10.0 moves the vanished 02:30 run to 03:30 and fires the daily 01:30 job once rather than twice. Both are reasonable choices for a scheduler. Neither is what an unconfigured cron does.

On the cron macOS ships, the better behaviour exists and is switched off. man 8 cron documents a -s flag enabling “special handling of situations when the GMT offset of the local timezone changes”, under which a job caught in a vanished interval runs “at the same absolute point of time as they would be in the old time zone” — and -o, which disables it “to be compatible with the old (default) behavior”. Default means off, and /System/Library/LaunchDaemons/com.vix.cron.plist launches /usr/sbin/cron with no arguments. So a simulator that prints 03:30 for that job, with no note, has answered a question about a flag it never asked you about. The manual is blunter than either of us: “In general, it is not a good idea to schedule jobs during this period.”

Steps restart; they do not repeat

A step is a filter over a field’s range, not an interval, and the range restarts at every boundary. That makes a whole family of expressions mean something other than what they read like, and the gap column above is how you see it without taking anyone’s word:

You writeIt reads likeIt actually fires at
*/45 * * * *every 45 minutes:00 and :45 — gaps of 45 minutes and 15
*/61 * * * *every 61 minutes:00 only — it is hourly
0-59/13 * * * *every 13 minutes:00 :13 :26 :39 :52 — then an 8-minute gap
0 */5 * * *every 5 hours00 05 10 15 20 — then a 4-hour gap
0 0 */7 * *every 7 daysthe 1st, 8th, 15th, 22nd, 29th — then the 1st again

cronstrue 3.24.0 calls the first two “Every 45 minutes” and “Every 61 minutes”. Where a step does not divide its field evenly this page adds a line saying so and naming the short gap; where it does — */15, */20, 0 */2 * * * — it says nothing, because there is nothing to warn about.

Input this page refuses, and why refusing is the feature

The two worst inputs are the ones that look like nothing at all. Measured against cron-parser 5.10.0: the empty string parses, and schedules something every minute; three fields parse too, read as seconds, minutes and hours. That is how a truncated template variable or a half-finished edit becomes a job that runs 1,440 times a day, and it is why this page counts the fields before it does anything else and names the ones that are missing.

The other half of the same principle is that a refusal has to say what is wrong, at the character it is wrong at. A caret under the offending field beats a sentence: five fields on one line is exactly the case where pointing is clearer than prose. So 0 0 * * 8 gets “8 is outside the day of week range 0–7. 0 and 7 are both Sunday” with the caret under the 8, and 0 22-2 * * * gets told that cron ranges do not wrap, along with the 22-23,0-2 that does what was meant.

Two refusals are deliberately not errors. @reboot is a valid crontab entry with no schedule in it, so it gets the answer rather than a message about months; 15W — the nearest weekday to the 15th — is named as the Quartz extension it is, rather than reported as a syntax error, because knowing whose syntax you have written is the thing you needed. L, L-3, 5L and 5#3 all work here and are marked as the same kind of extension: Java schedulers have them, POSIX cron does not.

The sparsest schedule the format can express

0 0 * 2 5#5 is the fifth Friday of February. February has a fifth Friday only when it has 29 days and the 29th is a Friday, which happens in 2036, 2064, 2092, 2104 — and then, across the century, not again between 2188 and 2228. A forty-year gap, from an expression that is fourteen characters long and perfectly legal.

That number is why the search behind this page walks a hundred years rather than the four a leap-day schedule seems to need. It can afford to because the day fields contain no timezone: the three of them are pure calendar arithmetic, so the search steps integer day numbers and only asks the browser for a UTC offset once a day actually matches. A century of days costs about a millisecond. Stepping minute by minute over the same span, which is the obvious implementation, costs nearly four seconds. cron-parser finds the 2036 run and then throws “Invalid expression, loop limit exceeded” reaching for the one after it.

The neighbouring case is 0 0 29 2 *, which is a real schedule with a real answer: after 29 February 2096 the next run is 29 February 2104, because 2100 is not a leap year. And 0 0 30 2 * has no run at all, ever — this page says so immediately, by inspecting the fields rather than by stepping a century to prove a negative.

The same schedule in four places

crontab · the preamble everyone forgets
# crontab -e. The preamble everyone forgets, and why.
#
# man 5 crontab documents exactly four variables cron sets for you: SHELL (/bin/sh),
# HOME and LOGNAME (from /etc/passwd), and MAILTO (only if it has mail to send).
# PATH is not on that list and is not mentioned anywhere in the page — which is why
# "it works in my shell but not in cron" is almost always PATH, and why the first two
# lines of a working crontab are these:
SHELL=/bin/bash
PATH=/usr/local/bin:/usr/bin:/bin
MAILTO=""            # empty means send no mail at all; unset means mail the owner

# min hour dom mon dow  command
0    9    *   *   1-5   /opt/app/bin/report >> /var/log/report.log 2>&1

# A comment may not sit on the same line as a command — it becomes part of the
# command. Same for an environment line. Both are in the man page's first paragraph.
0    3    *   *   0     /opt/app/bin/vacuum   # this comment is an argument to vacuum
crontab · running a job in another timezone
# "How do I run cron in another timezone" — the answer on most Linux crons.
#
# CRON_TZ applies to every line BELOW it in the file, so put it at the top or accept
# that the lines above it are still running in the system zone. cronie's crontab(5)
# documents it as "the time zone specific for the cron table"; it is an extension,
# and the cron macOS ships does not have it — its crontab(5) never mentions a zone.
CRON_TZ=Europe/Riga

0 9 * * 1-5  /opt/app/bin/report      # 09:00 in Riga, whatever the host is set to

# Without it, cron uses the system's local time — so the schedule silently moves when
# somebody changes /etc/localtime, and a job written on a laptop lands an hour out on
# a server in UTC. The portable version of "run at 09:00 Riga time" on a UTC host is
# to do the arithmetic yourself and accept that it breaks twice a year:
0 6 * * 1-5  /opt/app/bin/report      # 09:00 EEST — but 08:00 Riga time in winter
kubernetes · a CronJob, and three differences from a crontab
# Kubernetes CronJob. Three differences from a crontab, all documented.
apiVersion: batch/v1
kind: CronJob
metadata:
  name: report
spec:
  schedule: "0 9 * * 1-5"
  # 1. Without timeZone, the API reference says the schedule runs in "the time zone of
  #    the kube-controller-manager process" — not, as is often assumed, UTC. In
  #    practice that is usually UTC, but it is a property of your control plane rather
  #    than a guarantee, and naming the zone removes the question.
  timeZone: "Europe/Riga"
  concurrencyPolicy: Forbid
  startingDeadlineSeconds: 300
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: OnFailure
          containers:
            - name: report
              image: registry.example.com/report:1.4.2

# 2. The Kubernetes docs give the day-of-week range as 0-6, Sunday to Saturday.
#    man 5 crontab gives 0-7, with 0 and 7 both Sunday. "0 0 * * 7" is a valid crontab
#    line and is outside the range the CronJob docs describe.
# 3. There is no @reboot. The other seven @ strings are listed, @midnight included.
systemd · a different syntax, and a frequent destination
# systemd timers, which is where a lot of these schedules end up on modern Linux.
# Different syntax, and the fields read left to right in calendar order instead of
# smallest-unit-first: DayOfWeek Year-Month-Day Hour:Minute:Second.

# /etc/systemd/system/report.timer
[Unit]
Description=Weekday report

[Timer]
OnCalendar=Mon..Fri *-*-* 09:00:00
Persistent=true        # run once on boot if the machine was off when it was due
Unit=report.service

[Install]
WantedBy=timers.target

# systemd.time(7) documents shorthands — minutely, hourly, daily, weekly, monthly,
# quarterly, semiannually, yearly — and says a calendar event uses local time unless
# the expression names a zone or ends in UTC. Check what you wrote before enabling it:
#   systemd-analyze calendar 'Mon..Fri *-*-* 09:00:00' --iterations=5
# That prints the next five times, which is what the table at the top of this page is.

What this page will not do

It never schedules anything, never notifies, and never keeps state — it is a simulator, and everything it computes runs in this tab, which you can check for yourself. It does not parse a whole crontab file with its environment lines and comments; that is a genuinely useful and genuinely different tool. It does not accept systemd’s OnCalendar or Quartz as first-class syntaxes — both appear in the snippets above and neither is parsed. And it will not tell you when a job last ran: that is an operations question that needs the machine’s own history, and any answer computed from the expression alone would be a guess dressed as a record.

The specifications this page follows

  • man 5 crontab — the five-field format, the 0–7 day-of-week range with 0 and 7 both Sunday, the eight @ strings, the three-letter month and day names, and the one sentence defining the day-field OR rule. Also the note that ranges and lists of names are not allowed on this cron, which is why MON-FRI carries a portability note here even though Debian and cronie accept it.
  • man 8 cron — the -s and -o flags, which decide what happens at a daylight saving transition and which default to off.
  • cronie’s crontab(5) — CRON_TZ, and the sentence that missing times never match while repeated times run twice. That is the behaviour modelled above.
  • The Quartz extensions — L, L-3, 5L, 5#3, ?, the seconds field and the year field. Supported here and marked as extensions; W and LW are named and not implemented.
  • The Kubernetes CronJob API reference — .spec.timeZone, and the default of “the time zone of the kube-controller-manager process”.
  • systemd.time(7) — OnCalendar, quoted in the snippet above and deliberately not parsed.

FAQ

Why does 0 0 13 * 5 run on every Friday and on the 13th?

Because the two day fields are OR, not AND, and this is the single most surprising rule in the format. It is documented, in one sentence, which is why almost nobody has read it — man 5 crontab: "If both fields are restricted (ie, are not *), the command will be run when either field matches the current time." The manual gives its own example: 30 4 1,15 * 5 runs at 4:30am on the 1st and 15th of each month, plus every Friday. So 0 0 13 * 5 is not "midnight on Friday the 13th" — starting 1 April 2026 it fires on the 3rd, the 10th, the 13th, the 17th and the 24th. Five runs in a month where the natural reading predicts none, because 13 April 2026 is a Monday. The rule only applies when both day fields are restricted: with either one left as *, the other decides alone. And it applies to the day fields only — the month field is still ANDed, so 0 0 29 2 1 runs on February's Mondays and not on Mondays in March. If you want the real "Friday the 13th" schedule, cron cannot express it in one line; the usual answer is 0 0 13 * * with a test on the day of week inside the command.

What does */7 in the day-of-month field actually do?

It fires on the 1st, 8th, 15th, 22nd and 29th of every month, and then restarts — so the gap between the 29th and the next run is three days in January, two in a 30-day month, and one after a leap February. It is not "every 7 days". A step is a filter over a fixed range, not an interval: */7 in a field that runs 1 to 31 means "every value from 1 to 31 that is 7 apart from 1", and the range restarts at each month boundary. The same trap has sharper versions in the minute field, where the range is 0 to 59 and does not divide by every number you might pick. */45 is not every 45 minutes: it fires at :00 and :45, so the gaps alternate 45 minutes and 15. */61 is not every 61 minutes: 61 is past the end of the field, so the only value it produces is 0, and the schedule is hourly. 0-59/13 fires at :00, :13, :26, :39 and :52 and then waits eight minutes. cronstrue 3.24.0 — the library this page writes its own descriptions with — calls the first "Every 45 minutes" and the second "Every 61 minutes". The gap column in the table above is there so you never have to take anyone's sentence for it.

Which timezone does cron use, and how do I change it?

Plain cron uses the machine's local time, which means the schedule is a property of the host and not of the crontab — change /etc/localtime and every job moves. There is no timezone field in the five-field format, and the crontab(5) manual on macOS never mentions a zone at all. On Linux crons derived from cronie, the answer is CRON_TZ: cronie's crontab(5) describes it as "the time zone specific for the cron table", and it applies to the lines below it in the file, so it belongs at the top. Elsewhere the answer is a property of the scheduler rather than of cron: a Kubernetes CronJob has .spec.timeZone, and its API reference says that without it the schedule runs in "the time zone of the kube-controller-manager process" — usually UTC, but a fact about your control plane rather than a promise. A systemd timer's OnCalendar uses local time unless the expression says otherwise. The zone selector on this page is the whole reason it exists: pick the zone the scheduler will actually use, not the one your laptop is in, and the run times above change accordingly.

What happens to a cron job when the clocks change?

A cron schedule is a wall clock, so twice a year some wall clocks either do not exist or exist twice, and what a scheduler does about that is a choice it makes rather than arithmetic. The behaviour this page models is the one cronie's crontab(5) documents: "non-existent times, such as the missing hours during the daylight savings time conversion, will never match, causing jobs scheduled during the missing times not to be run. Similarly, times that occur more than once will cause matching jobs to be run twice." So in America/New_York a job at 30 2 * * * does not run at all on 8 March 2026, and a job at 0 * * * * runs twice on 1 November 2026, once at 01:00 EDT and once at 01:00 EST an hour later. The cron macOS ships can do better, but only if asked: man 8 cron documents a -s flag that enables "special handling of situations when the GMT offset of the local timezone changes", under which a job in a vanished interval runs "at the same absolute point of time as they would be in the old time zone" — and -o, which disables it "to be compatible with the old (default) behavior". Default, in that sentence, means off. macOS launches /usr/sbin/cron with no arguments at all, which is checkable in /System/Library/LaunchDaemons/com.vix.cron.plist. The manual's own BUGS section is blunter than any of this: "In general, it is not a good idea to schedule jobs during this period."

Why does @reboot have no next run?

Because it has no clock in it. @reboot is one of the eight special strings in man 5 crontab's table, and unlike the other seven it does not expand into five fields — @daily is "0 0 * * *" and @hourly is "0 * * * *", but @reboot means "run once, at startup". There is no time of day to compute and no next occurrence to show, so this page says that rather than reporting an error, which is what the library it is tested against does: cron-parser 5.10.0 throws "Validation error, cannot resolve alias" on it. Two practical consequences. The first is that @reboot fires when cron starts, not when the machine finishes booting — if your job needs the network or a mounted volume, cron makes no promise that either exists yet, and a systemd unit with the right After= is the honest tool. The second is that @reboot is the one @ string that does not travel: the Kubernetes CronJob docs list the other seven and cannot express this one, because there is no reboot for a controller to observe.

Why does my six-field expression mean something different here?

Because six fields is ambiguous and the two readings disagree by a factor of sixty. A crontab line has five fields, minute first. Quartz, Spring and node-cron take six, with seconds first — so 0 0 12 * * ? is noon in those and is not a crontab line at all. A sixth field can also be a year, which is Quartz's seventh field written without a seconds column, so 0 0 12 * * 2026 is midnight on the 12th of each month during 2026. This page decides between the two by looking at the field rather than guessing: 2026 cannot be a day of week, so that reading is year-last; a ? or a 0-7 value can only be the other one. It says which reading it took, and where both readings are valid it offers the flip. The related trap is the other direction. cron-parser 5.10.0 accepts three fields by reading them as seconds, minutes and hours, and accepts the empty string as * * * * *. Both mean that a typo which should have been refused becomes a job that runs every minute. This page counts the fields first and names the ones that are missing.

related tools