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
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
Description
CronTime.getNextDateFrom(start, timeZone)returns an instant earlier thanstartwhenstartis 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)andCronTime.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, andgetNextDateFromshould never return something at or before itsstart.Actual Behavior
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 as05: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 atSettings.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 > startand the half hour variant) only ever moves the result earlier, so nothing pushes it paststartand 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
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
cronversion: 4.4.0 (luxon 3.7.2)