How to validate cron expression

Do not read a cron expression by eye. Count the fields, then print the next runs with node -e and cron-parser. Five fields are minute, hour, day of month, month, day of week, so 0 3 * * * from 11 September 2026 fires next at 2026-09-12T03:00:00Z, once a day in UTC.

Why check this

A wrong cron expression parses cleanly and runs on the wrong schedule, so nothing fails and no alert fires. The bill arrives later: a reconciliation job written as a daily task sends 24 reports a day, or a cleanup job set to 03:00 in a UTC container starts inside the local backup window. Check the expression when a scheduled job is added or edited, and again when a service moves to another region.

Prerequisites

Steps

  1. Step 1.

    Count the fields before parsing anything.

    echo '0 3 * * *' | wc -w
    
    5

    Five is standard cron. Six means the first field is seconds, which Quartz and node-cron accept and crontab rejects.

  2. Step 2.

    Print the next three runs from a fixed instant, so the result is repeatable.

    node -e "const { CronExpressionParser } = require('cron-parser'); const i = CronExpressionParser.parse('0 3 * * *', { currentDate: '2026-09-11T10:00:00Z', tz: 'UTC' }); for (let n = 0; n < 3; n++) console.log(i.next().toDate().toISOString());"
    
    2026-09-12T03:00:00.000Z
    2026-09-13T03:00:00.000Z
    2026-09-14T03:00:00.000Z

    The gap between runs is 24 hours, which is the answer to what the schedule means.

  3. Step 3.

    Run the same check on an expression that looks daily and is not.

    node -e "const { CronExpressionParser } = require('cron-parser'); const i = CronExpressionParser.parse('30 * * * *', { currentDate: '2026-09-11T10:00:00Z', tz: 'UTC' }); for (let n = 0; n < 3; n++) console.log(i.next().toDate().toISOString());"
    
    2026-09-11T10:30:00.000Z
    2026-09-11T11:30:00.000Z
    2026-09-11T12:30:00.000Z

    One hour apart, 24 runs a day. The minute is pinned and the hour is a wildcard, so 30 * * * * means half past every hour.

  4. Step 4.

    Print the same expression in the zones the job might run in.

    node -e "const { CronExpressionParser } = require('cron-parser'); for (const tz of ['UTC', 'Europe/Kyiv', 'America/New_York']) console.log(tz.padEnd(17), CronExpressionParser.parse('0 3 * * *', { currentDate: '2026-09-11T10:00:00Z', tz }).next().toDate().toISOString());"
    
    UTC               2026-09-12T03:00:00.000Z
    Europe/Kyiv       2026-09-12T00:00:00.000Z
    America/New_York  2026-09-12T07:00:00.000Z

    One expression, three different instants. The zone is part of the schedule, not a display setting.

  5. Step 5.

    Hand the parser four fields and read what it decided to do with them.

    node -e "const { CronExpressionParser } = require('cron-parser'); const i = CronExpressionParser.parse('0 3 * *', { currentDate: '2026-09-11T10:00:00Z', tz: 'UTC' }); console.log(i.stringify(), '->', i.next().toDate().toISOString());"
    
    0 0 3 * * -> 2026-10-03T00:00:00.000Z

    The library filled the missing field on the left, so 0 3 * * became midnight on the third of the month. Step 1 exists because of this.

  6. Step 6.

    Set the day of month and the day of week together and check which days match.

    node -e "const { CronExpressionParser } = require('cron-parser'); const i = CronExpressionParser.parse('0 0 13 * 5', { currentDate: '2026-09-11T10:00:00Z', tz: 'UTC' }); for (let n = 0; n < 3; n++) { const d = i.next().toDate(); console.log(d.toISOString(), d.getUTCDay()); }"
    
    2026-09-13T00:00:00.000Z 0
    2026-09-18T00:00:00.000Z 5
    2026-09-25T00:00:00.000Z 5

    The 13th is a Sunday here and it still matched. The two day fields combine with OR, not AND, so this expression means every Friday and every 13th.

  7. Step 7.

    Check an expression that names a date the calendar does not have.

    node -e "const { CronExpressionParser } = require('cron-parser'); try { CronExpressionParser.parse('0 0 30 2 *', { tz: 'UTC' }); } catch (e) { console.log(e.message); }"
    
    Invalid explicit day of month definition

How to read the result

| What you see | What it means | What to do | | --- | --- | --- | | Runs 24 hours apart at the same clock time | A daily job | Nothing. Record the zone next to the expression. | | Runs one hour apart, same minute | The hour field is a wildcard | Put the hour in field two. Field one is the minute. | | Runs one minute apart | Every minute, 1440 times a day | Confirm the job finishes in under a minute, or runs overlap. | | The times change when you name a zone | The scheduler's zone decides the wall clock | Pin the zone in the job definition instead of relying on the host. | | The parser prints back a different expression | The field count was wrong and got padded | Fix the expression. crontab rejects the same line the library accepted. | | Invalid explicit day of month definition | No calendar date matches | Correct the day or the month. The job would never have run. |

Common mistakes

Sign: A job described as daily sends 24 reports a day.Cause: The expression is 30 * * * * . The minute was set and the hour left as a star, which reads as half past every hour. Printing the first three run times, as step 3 does, shows a one hour gap immediately.
Sign: The expression validates in a library and crontab refuses the line.Cause: Parsers are more forgiving than cron. cron-parser 5.4.0 accepts four fields and pads on the left, and it accepts a six field seconds dialect that Linux cron and Kubernetes CronJob both reject. Count the fields yourself before trusting the parse.
Sign: A job set for Friday the 13th also runs every Friday and every 13th.Cause: When both the day of month and the day of week are restricted, cron matches either one, not both. Vixie cron has behaved this way since the 1980s and the manual page documents it, so the fix is a day check inside the job.
Sign: The nightly job fires an hour early for part of the year.Cause: The schedule is stored in a named zone that observes daylight saving. A run at 02:30 local can be skipped entirely on the spring transition and repeated on the autumn one. Jobs that must not repeat are scheduled in UTC.

What to check next

FAQ

How to check the next cron run time?

Parse the expression with a fixed starting instant and print the next runs, as step 2 does. A fixed instant makes the result repeatable, so the same check in CI gives the same dates on any machine.

How to read a cron expression?

Left to right: minute, hour, day of month, month, day of week. A star means every value of that field. Reading is the slow way to be wrong, which is why every step here prints dates instead.

How to validate a cron expression online?

The checker at the top of this page parses in the browser and lists the next five runs with the field count. The expression is not sent anywhere. It defaults to UTC and says so on the result row.

Does cron support seconds?

Standard cron does not. Six fields are a Quartz and node-cron extension where the first field is seconds. Linux crontab and Kubernetes CronJob reject them, so a six-field line copied from an application scheduler fails on a server.

Verified

Verified by Maks Vernynode 22.23.2cron-parser 5.4.0bash 5.2.15

Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.

basic4 minpublished updated Maks Verny