Skip to content

fix: getNextMatch() returns past dates near DST boundaries#508

Open
GabeK0 wants to merge 3 commits into
node-cron:mainfrom
GabeK0:fix/dst-boundary-getNextMatch
Open

fix: getNextMatch() returns past dates near DST boundaries#508
GabeK0 wants to merge 3 commits into
node-cron:mainfrom
GabeK0:fix/dst-boundary-getNextMatch

Conversation

@GabeK0
Copy link
Copy Markdown

@GabeK0 GabeK0 commented Mar 8, 2026

Summary

getNextMatch() / getNextRun() can return timestamps in the past near DST transitions, causing infinite "missed execution" warnings in the scheduler.

Two bugs in localized-time.ts:

  1. getTimezoneGMT() derives offsets via new Date(date.toLocaleString(...)) roundtrip — the re-parsed string picks up the host timezone's offset at that wall-clock time, not the target timezone's. Replaced with Intl.DateTimeFormat('en-US', { timeZone, timeZoneName: 'shortOffset' }) to parse the offset directly.

  2. set() doesn't account for DST boundary crossings. When setting a field moves the date across a DST transition, the offset changes and local time drifts by the offset delta. Added drift detection and timestamp correction after rebuilding parts.

Reproduction

const { TimeMatcher } = require('node-cron/dist/cjs/time/time-matcher.js')
const tm = new TimeMatcher('* * * * *')
// Run with TZ=America/New_York
console.log(tm.getNextMatch(new Date('2026-03-07T22:32:42-05:00')).toString())
// Before fix: Sat Mar 07 2026 21:33:00 GMT-0500 (1 hour in the past)
// After fix:  Sat Mar 07 2026 22:33:00 GMT-0500 ✅

Verified across America/New_York, America/Los_Angeles, America/Phoenix, and UTC.

Test plan

  • * * * * * in DST-observing host TZ returns future timestamp
  • */5 * * * * in DST-observing host TZ returns future timestamp
  • 0 7 * * * with { timezone: 'America/New_York' } returns next day (~8.5 hours), not Jan 2027
  • All above pass in UTC and non-DST host timezones
  • Build passes (npm run build)

Replace lossy toLocaleString roundtrip in getTimezoneGMT() with
Intl.DateTimeFormat shortOffset parsing. Add DST drift compensation
in set() to preserve intended local time across boundary crossings.
import BackgroundScheduledTask from "./background-scheduled-task";

import { EventEmitter } from 'stream';
import { EventEmitter } from 'events';
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'stream' does not export EventEmitter as a named ESM export; use 'events' instead. Fixes some broken tests

@GabeK0 GabeK0 force-pushed the fix/dst-boundary-getNextMatch branch from 90164a9 to f989527 Compare March 8, 2026 06:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant