Back to News & Insights
JavaScript October 10, 2026 · 7 min read

Node.js 26's default Temporal API fixes a one-hour DST drift that Date still has

Node.js 26 enables the Temporal API with no flag. I found it fixes a real one-hour DST drift in naive Date arithmetic, but costs about 19 times more than Date per operation, and its default overflow mode doesn't throw where I expected it to.

Node.js 26's default Temporal API fixes a one-hour DST drift that Date still has

tomorrow = new Date(now.getTime() + 24 60 60 1000) is a line that turns up in a lot of scheduling and billing code. It looks harmless, and for most of the year it is. Twice a year, in any timezone that observes daylight saving, it's wrong by an hour.

Node.js 26, released in May, turns on the Temporal API without a flag. The release notes describe it as a "modern date/time API for JavaScript that provides a more robust and feature-rich alternative to the legacy Date object." I pulled the official image, ran the naive pattern next to Temporal.ZonedDateTime.add(), and the drift showed up on the first try.

I wanted to know what that default actually buys a working codebase, not just what the API looks like in isolation, so everything below is a real run against the official node:26 and node:24 images, not a reading of the spec.

I picked the 2026 US spring-forward (2026-03-08, clocks in America/NewYork jump from 02:00 to 03:00) and started both calculations from the same instant: 2026-03-07 at 22:00 local time.

| | result | |---|---| | start (local wall clock) | 03/07/2026, 22:00:00 | | Date + 24h, read back as local time | 03/08/2026, 23:00:00 | | Temporal zdt.add({days: 1}) | 2026-03-08T22:00:00-04:00 |

The Date version lands an hour later than it should, because adding 24 hours of UTC time walks straight through the hour that the clocks skipped. Temporal keeps the wall-clock time fixed at 22:00 and lets the UTC offset move from -05:00 to -04:00 instead, which is what "the same time, one calendar day later" actually means to a person. That's the whole value proposition in one run, and it's the first thing that didn't need three attempts to show up.

Correct math isn't free. I ran a million iterations of "take an instant, add a second to it, read a field back" for both APIs, three times each, on an otherwise idle container:

| trial | Date (ms) | Temporal (ms) | |---|---|---| | 1 | 65.4 | 2071.5 | | 2 | 72.8 | 1839.7 | | 3 | 66.5 | 1913.1 |

Roughly 27 times slower, consistently, not a one-off. That number felt too clean, so I split the loop to find out which half of the operation was actually responsible, rather than trust the combined figure.

Isolated, .add() alone was 59.6ms for Date against 1152.9ms for Temporal — about 19 times slower on its own. Reading a field off an already-built ZonedDateTime (zdt.hour) was a smaller gap, 11.5ms against 71.9ms. So the earlier combined number wasn't an artefact of my timing window catching something unrelated; .add() really is the expensive part, and it's expensive by design, not by my mistake. Temporal objects are immutable and carry full calendar and timezone context through every operation, where Date is just a wrapped 64-bit number.

I went in expecting Temporal to be the strict, no-silent-surprises alternative to Date's habit of normalising invalid input. new Date(2026, 1, 30) quietly becomes March 2nd, because February doesn't have a 30th day and Date just keeps counting. I assumed Temporal.PlainDate.from() would refuse that outright.

It doesn't throw. The default overflow mode is "constrain", which clips the out-of-range day down to the last valid one instead of raising anything. It's a different kind of wrong to Date's (clipping to the 28th is arguably more defensible than rolling into March), but it is still silent, and that contradicted what I expected walking in. Getting a real error needs an explicit option:

A genuinely malformed string throws without needing that option, so the strictness is there, just not where I first looked for it:

If any of your code treats "no exception" as "the date was valid," Temporal's default construction from a plain object won't give you that for free. You still need overflow: 'reject'.

Node 24, the LTS line Node 26 will eventually replace, doesn't have Temporal at all without an explicit opt-in:

Adding --harmony-temporal on Node 24.21.0 gets you the same global, and in my checks it behaved identically to Node 26's default — same overflow rules, same error text, same arithmetic results. So the actual change in 26 is narrower than "Temporal arrived": it was already there, behind a flag, since at least the 24.x line, sitting on V8 13.6. What 26 changes is who gets it without reading the release notes first, and it comes bundled with the V8 jump to 14.6 rather than a Temporal-specific rewrite. The flag works the other way too: Node 26 exposes --no-harmony-temporal if you need Temporal to stay undefined, which matters if a library you depend on polyfills the name and would conflict with the native one.

I also checked whether there's anything to watch while this runs in production — a counter, a log line, a flag you can read back at runtime. process.features doesn't mention it at all, and process.config.variables does list v8enabletemporalsupport and v8enabletemporal_systemicu, both 1 on the official image — but those are compile-time build flags, not a live state: they stayed 1 even when I ran the same process with --no-harmony-temporal, which does turn the global off. So neither surfaces the actual runtime toggle. The only reliable check from inside a process is typeof Temporal, which is a reasonable thing to assert in a startup health check if you're migrating code onto it deliberately.

Turning a global on by default raised an obvious question: does every Node 26 process now pay for Temporal even if it never touches it? I spawned twenty bare node -e "1" processes with the default flags and twenty more with --no-harmony-temporal, timing each with process.hrtime.bigint() around the spawn:

Want to discuss this further?

Book a free strategy call with our team to see how these insights apply to your specific business goals.

Book a consultation