#1087·node-cron

getNextDateFrom(date, timeZone) returns a date before `date` inside the repeated hour of a fall-back transition (regression in 4.1.1)

Author: ChangkeunJCreated Sep 8, 2026Updated Sep 8, 2026

Description

CronTime.getNextDateFrom(start, timeZone) returns an instant earlier than start when start is in the first half of the repeated hour of a fall-back DST transition and the call is made before that transition. CronJob.nextDates(n) and CronTime.sendAt(n) chain through it, so they hand back a list that runs backwards over that hour. The scheduling loop itself isn't affected, see Context.

Bisected: 4.1.0 is fine, 4.1.1 and every published version since returns the earlier instant. The only src change between those tags is 8cf0712 (#966), luxon is 3.5.0 on both sides. #1049 describes a similar symptom, but its repro imports node-cron/node-cron; this one is against this package.

Expected Behavior

For 30 1 * * * in America/New_York, asked from 2026-11-01T06:00:00Z (01:00 EST, the second 01:00 of the night), the next date is 2026-11-01T06:30:00Z (01:30 EST). That's what 3.5.0, 4.0.0 and 4.1.0 return, and getNextDateFrom should never return something at or before its start.

Actual Behavior

2026-11-01T06:00:00Z -> 2026-11-01T05:30:00.000Z 30 min before start
2026-11-01T06:15:00Z -> 2026-11-01T05:30:00.000Z 45 min before start
2026-11-01T06:29:00Z -> 2026-11-01T05:30:00.000Z 59 min before start

05:30Z is 01:30 EDT, the first occurrence, already in the past. Through nextDates(8) on * * * * * with the clock at 05:58Z it comes out as 05:59Z 06:00Z 05:01Z 05:02Z 05:03Z ....

Possible Fix

The search runs on a UTC clone and the result is rebuilt with DateTime.fromFormat(..., { zone: timeZone }). For a wall time that exists twice, luxon picks the occurrence by seeding with the zone offset at Settings.now() (fromObject, offsetProvis = zone.offset(tsNow), datetime.js line 800 in 3.7.2). Before the transition that's EDT, so 01:30 becomes 05:30Z. The ambiguity handling that follows (hourTestDate > start and the half hour variant) only ever moves the result earlier, so nothing pushes it past start and 05:30Z is returned as is. Once the real clock is past the transition luxon seeds with EST and the same call returns 06:30Z, which is why this hides most of the year.

I have a fix that returns the later occurrence when the resolved date is <= start, with a regression test that pins the clock before the transition so it fails on current main whatever day it's run. Full suite 167/167 under TZ=Europe/Paris, lint clean. Happy to open the PR.

Steps to Reproduce

javascript
import { CronTime } from 'cron';
// America/New_York falls back on 2026-11-01: 01:00-02:00 EDT (05:00-06:00Z) is followed by 01:00-02:00 EST (06:00-07:00Z)
const t = new CronTime('30 1 * * *', 'America/New_York');
for (const iso of ['2026-11-01T06:00:00Z', '2026-11-01T06:15:00Z', '2026-11-01T06:29:00Z']) {
  const start = new Date(iso);
  const next = t.getNextDateFrom(start, 'America/New_York').toJSDate();
  console.log(iso, '->', next.toISOString(), next < start ? `${(start - next) / 60000} min before start` : '');
}

Same output under TZ=UTC, TZ=America/New_York and TZ=Asia/Seoul. Run it before 2026-11-01 (or with the clock faked to any EDT date) to see it; after the transition the same call is correct.

Context

I was comparing how schedulers handle the repeated hour and this one came back with a date before its input. What isn't affected: a running job calls sendAt() from the current clock, and by the time it's inside the repeated hour luxon already seeds with the new offset. Faking the clock at 1 minute steps across the fall-back night in America/New_York, Australia/Sydney and Australia/Lord_Howe for 7 expressions (4,851 calls) never returned a time at or before now, and jobs run through the transition on sinon fake timers fired 240/240 minutes with no warnings and no duplicates. So this is about the "next N dates" answers, not about jobs firing wrong in production.

Your Environment

  • cron version: 4.4.0 (luxon 3.7.2)
  • NodeJS version: v20.20.2
  • Operating System and version: Ubuntu, Linux 5.15
  • TypeScript version (if applicable): n/a, plain ESM
  • Link to your project (if applicable): n/a