obfus.link
Converters

Humanizing cron expressions + conflict detection across crontabs

Translate any cron expression to plain English with next-run computation, and detect collisions and resource-contention windows across an array of cron expressions. Catches scheduling conflicts before they hit production.

The Cron Humanizer translates any cron expression into plain English with parsed field breakdowns and optional next-run computation in a chosen timezone. Conflict-detection mode accepts an array of cron expressions and identifies collisions (exact same time) and adjacent windows (within 5 minutes). Wires into CI as a pre-deploy gate.

1. Insight

Insight

The problem this article addresses and why it matters.

Cron is write-once, debug-frequently

Cron is one of those formats every developer can write but few can read aloud. 0 9 * * 1-5 is "weekdays at 9 AM" once you know the field order; */15 9-17 * * 1-5 is "every 15 minutes between 9 and 5 PM on weekdays". The expressions get committed, the team moves on, and three months later someone needs to debug why a job ran at the wrong time — and now they're re-deriving the field order from man page memory.

The harder problem is conflict detection across crontabs. A single team's crontab is manageable. Across a microservice deployment with dozens of services each scheduling its own jobs, two services hitting the same time window can cascade: both run at 0 2 * * *, both contend for the same database connection pool, both timeout, both alert on-call. The conflict was inevitable; nothing in the crontab tooling surfaces it.

Why humanise + conflict-check in one tool

The tool in this article does two related jobs. Humanise mode takes a single cron expression and produces a plain-English description plus the next N run times. That's the everyday "what does this expression mean?" workflow. Conflict mode takes an array of expressions — typically every cron expression in the team's deployment — and identifies overlaps and adjacencies.

The conflict detector returns severity ratings: a collision is two expressions firing at the exact same time, an adjacent finding is two expressions firing within 5 minutes of each other (close enough to contend for shared resources). The output tells you which pairs of expressions overlap and when.

What this article delivers

Humanise mode walked against three expressions of varying complexity, conflict mode walked against a representative team crontab, and the next-run-time calculation against timezone-aware schedules. We cover the edge cases around DST transitions, the ? and L extensions that some cron parsers support, and the cases where the tool's output should be sanity-checked against the actual scheduler's behaviour.

2. Intent

Intent

What you will be able to do after reading.

By the end of this article you will be able to:

  • Translate any cron expression into plain English with parsed field breakdowns
  • Compute the next N run times for a schedule in a chosen timezone, with DST awareness
  • Run conflict-detection across an array of cron expressions to find collisions and resource-contention windows
  • Read the conflict report's severity field — collision (exact same time) vs adjacent (within 5 minutes)
  • Recognise the cron extensions (?, L, W, #) that some parsers support and others reject

The Examples section walks through three expressions in humanise mode and a representative team crontab in conflict mode.

3. Examples

Examples

Annotated code and worked scenarios.

Before / after: humanising three expressions

cronHumanizer({
  expression: '0 9 * * 1-5',
  count:      3,
  timezone:   'America/New_York',
});

// human: 'Every weekday at 9:00 AM (America/New_York)'
// valid: true
// nextRuns: [
//   '2026-05-19T13:00:00.000Z',  // Monday 9:00 ET
//   '2026-05-20T13:00:00.000Z',
//   '2026-05-21T13:00:00.000Z',
// ]
// parts: { minute: '0', hour: '9', dayOfMonth: '*', month: '*', dayOfWeek: '1-5' }

Plain English plus the next three runs in UTC. The parts breakdown is useful when debugging — you can see exactly which fields the parser interpreted.

A more complex expression:

cronHumanizer({
  expression: '*/15 9-17 * * 1-5',
});

// human: 'Every 15 minutes between 9:00 AM and 5:00 PM on weekdays'

And an edge case with explicit ranges:

cronHumanizer({
  expression: '0 0 1,15 * *',
});

// human: 'At midnight on the 1st and 15th of every month'

Before / after: conflict detection across a team crontab

A representative microservice crontab — five jobs across the day:

cronHumanizer({
  expression:    '0 2 * * *',           // self
  conflictCheck: [
    '0 2 * * *',                        // job A — same time
    '5 2 * * *',                        // job B — 5 min later
    '0 3 * * *',                        // job C — 1 hour later
    '0 2 * * 0',                        // job D — same time but only Sunday
  ],
});

// conflicts: [
//   {
//     expressionA:  '0 2 * * *',  expressionB: '0 2 * * *',
//     overlapWindow: 'Both run at 02:00 UTC every day',
//     severity:      'collision',
//   },
//   {
//     expressionA:  '0 2 * * *',  expressionB: '5 2 * * *',
//     overlapWindow: 'Run within 5 minutes of each other at 02:00 UTC every day',
//     severity:      'adjacent',
//   },
//   {
//     expressionA:  '0 2 * * *',  expressionB: '0 2 * * 0',
//     overlapWindow: 'Both run at 02:00 UTC every Sunday',
//     severity:      'collision',
//   },
// ]

Three conflict findings. The collision with job A is the immediate problem (two services both think they own the 2 AM window). The adjacent finding for job B is the resource-contention warning — if either job runs longer than 5 minutes, they'll overlap and contend for whatever shared resource they touch.

Before / after: timezone-aware next-run computation

DST transitions break naive cron scheduling. A cron expression 0 2 * * * set in America/New_York produces two valid runs on the fall-back day (2 AM happens twice) and zero valid runs on the spring-forward day (2 AM is skipped). The tool's count parameter respects this:

cronHumanizer({
  expression: '0 2 * * *',
  count:      10,
  timezone:   'America/New_York',
});

// nextRuns: [
//   '2026-11-01T05:00:00.000Z',  // 2 AM ET — first occurrence
//   '2026-11-01T06:00:00.000Z',  // 2 AM ET again after fall-back
//   '2026-11-02T07:00:00.000Z',
//   ...
// ]

For DST-sensitive jobs (financial reconciliation, log rotation), use UTC explicitly in the expression's interpretation by setting timezone: 'UTC'. The tool's next-run output then reflects clock time independent of DST behaviour.

Before / after: conflict-detection wired into CI

A pre-deploy check that runs against every cron expression in the service-mesh:

# In CI:
EXPRESSIONS=$(yq '.services[].schedule' deployment.yaml | jq -R . | jq -s .)
RESULT=$(curl -X POST https://obfus.link/api/v1/cron_humanizer \
  -d "$(jq -n --argjson exprs "$EXPRESSIONS" \
    '{expression: $exprs[0], conflictCheck: $exprs[1:]}')")
COLLISIONS=$(echo "$RESULT" | jq -r '.conflicts | map(select(.severity == "collision")) | length')
[ "$COLLISIONS" -eq 0 ] || { echo "Crontab collisions detected: $COLLISIONS"; exit 1; }

Now a new service whose schedule conflicts with an existing one fails the deploy. The conflict is caught at the deployment-config layer, not after the first 2 AM overlap actually triggers a database pool exhaustion.

When humans use this

The dominant use is reading: paste a cron expression you didn't write, get a plain-English description. The conflict-detection mode is the higher-leverage pre-launch check — run before adding a new scheduled job to verify it doesn't collide with an existing one. The next-run computation helps debug "did this job actually run?" investigations.

When agents use this

Two production patterns:

  • Cron PR review. An agent reviewing PRs to any service's cron schedule runs the conflict detector against the existing schedules in the codebase. Any collision or close-adjacent finding opens a PR comment with the specific overlapping expressions.
  • Infra-as-code validation. A pipeline that generates Kubernetes CronJobs from a YAML manifest runs every emitted schedule through the humaniser to verify the cron parser interprets it the same way the manifest author intended. Mismatches (e.g. 30 2 * * 1-5 mistakenly meaning "30 minutes past 2 AM" not "2:30 AM") surface as warnings.

Edge cases

Cron dialects

Standard cron has 5 fields (minute, hour, day-of-month, month, day-of-week). Some systems extend this: Quartz uses 6 or 7 fields (adding seconds and year), Kubernetes CronJobs use 5, AWS EventBridge uses 6 with ? placeholders. The tool defaults to 5-field standard cron; pass dialect: 'quartz' for Quartz extensions.

L, W, # extensions

Some parsers support L (last day), W (nearest weekday), # (Nth weekday of month). The tool surfaces these in humanise mode with a note that the syntax is non-portable — verify your scheduler supports it before using.

Day-of-week numbering

Standard cron uses 0-6 with 0 = Sunday. Some systems use 1-7 with 1 = Sunday or 1 = Monday. The tool defaults to standard; if your scheduler differs, the humanised description will be off by one day. Verify against your scheduler's docs.

Zero-padding behaviour

05 is the same as 5 in standard cron; some strict parsers reject the zero-padded form. The tool accepts both. The humanised output uses unpadded numbers.

4. Documentation

Documentation

Reference signatures, edge cases, and lookup tables.

Input parameters

Field

Type

Required

Default

Description

expression

string

A cron expression to humanise / inspect

count

number

0

Number of next-run timestamps to compute. 0 returns description only

timezone

string

'UTC'

IANA timezone for next-run computation

conflictCheck

string[]

Array of other cron expressions to check for overlaps

dialect

'standard' | 'quartz'

'standard'

5-field standard cron vs Quartz 6/7-field

Output shape

{
  human:     string;        // 'Every weekday at 9:00 AM'
  valid:     boolean;
  nextRuns?: string[];      // ISO timestamps when count > 0
  parts: {
    minute:     string;
    hour:       string;
    dayOfMonth: string;
    month:      string;
    dayOfWeek:  string;
    second?:    string;     // Quartz only
    year?:      string;     // Quartz only
  };
  conflicts?: Array<{       // when conflictCheck provided
    expressionA:   string;
    expressionB:   string;
    overlapWindow: string;
    severity:      'collision' | 'adjacent';
  }>;
}

Severity definitions

Severity

Trigger

collision

Both expressions fire at the exact same time at least once per common window

adjacent

Both expressions fire within 5 minutes of each other

Error codes

Code

When it fires

Recovery

INPUT_EMPTY

expression empty

Provide a cron expression

INPUT_MALFORMED

Expression doesn't parse as the chosen dialect

Verify field count + syntax matches dialect

INPUT_INVALID_TYPE

Field value out of range (minute > 59, hour > 23)

Fix the field

UNSUPPORTED_FORMAT

Expression uses an extension the dialect doesn't support (L in standard)

Switch dialect or rewrite without the extension

INPUT_TOO_LARGE

conflictCheck array exceeds 100 expressions

Split into multiple calls

When NOT to use this tool

For complex scheduling needs (rate-limited jobs, dependency-graph triggers, conditional runs), cron is the wrong primitive. Use a workflow engine (Airflow, Temporal, Prefect) where dependencies and conditions are first-class. The humaniser handles cron specifically.

For scheduling with sub-minute precision, standard cron tops out at 1-minute resolution. Quartz dialect supports seconds; the humaniser handles those, but most schedulers don't. Pick a sub-minute scheduler if you need it.

Performance notes

Typical execution: under 3ms for humanise + parts. Next-run computation: 1ms per future run requested, capped at 100 runs. Conflict detection: O(n²) in the conflictCheck array — 100 expressions runs in about 50ms. The tool is deterministic — same input always produces the same output — so REST responses are Edge-Cache eligible (cache key includes the timezone and current time bucket to keep next-run estimates fresh).

Try it now

Cron Humanizer

Parse cron expressions, compute next runs, and detect schedule conflicts

FAQ

Frequently asked questions

What's the difference between standard cron and Quartz?

Standard cron has 5 fields (minute, hour, dayOfMonth, month, dayOfWeek). Quartz adds seconds at the start and an optional year at the end (6 or 7 fields). The tool defaults to standard; pass dialect: 'quartz' for Quartz expressions.

Why is conflict detection important?

In a microservice deployment, two services scheduling jobs at the same time can both hit the same shared resource (database pool, third-party API rate limit, network egress). The conflict surfaces only when both run simultaneously — usually at 2 AM during nightly batch. Pre-deploy detection catches this before the outage.

What's the DST handling?

The next-run computation respects the configured timezone, including DST transitions. On fall-back days, 2 AM happens twice and the next-run output includes both. On spring-forward, 2 AM is skipped and the schedule jumps to 3 AM. For DST-insensitive jobs, set timezone: 'UTC'.

Does it support 6-field crontabs with seconds?

Yes via dialect: 'quartz'. Quartz adds seconds as the first field and an optional year as the seventh. Most container schedulers (Kubernetes CronJobs, AWS EventBridge) use 5-field standard cron; some Java schedulers (Spring @Scheduled with cron) use the 6-field form.