Node 26: What Breaks When You Upgrade, and When the Jump Is Worth It (LTS Guide)
Table of Contents
Node 26 shipped on May 5, 2026 and enters LTS in October. It’s a calm upgrade — the list of things that actually break is short, and almost all of it has been warned about for years — but it has one property no previous version had: it’s the last release of the two-per-year model. Starting with Node 27, the project moves to one release per year, and every release is LTS. The odd/even distinction that spent a decade deciding which version you could run in production disappears.
That makes this upgrade more than a version bump: it’s the last train on the old schedule, and it’s worth boarding knowing what’s inside. Here’s what really breaks, sorted by impact, and at the end the full calendar so you can decide when to move.
First: where are you coming from?
The effort depends on your starting point:
- From Node 24 (the current LTS): the comfortable scenario. The incompatible changes are few and very localized; for most projects the migration is checklist work, not refactoring work.
- From Node 22: still reasonable, but with more fronts — you absorb everything that changed in 24 (V8, permissions, minimum glibc) plus what’s new in 26. Node 22 leaves maintenance in April 2027, so you have a real deadline.
- From Node 25: odd releases were never for production, and this was the last one that will ever exist. Jump to 26 without thinking twice.
The schedule change: Node 27 debuts a new model
The official project announcement sums it up:
| Aspect | Through Node 26 | From Node 27 on |
|---|---|---|
| Major releases per year | 2 | 1 |
| LTS versions | Even-numbered only | All of them |
| Short-lived odd versions | Yes | Gone |
| Alpha channel | Didn’t exist | 6 months, allows semver-major changes |
| Total support per version | 36 months | 36 months (unchanged) |
The concrete dates that affect you:
- Node 26: LTS in October 2026, end of life in April 2029.
- Node 27: alpha channel opens October 2026, release in April 2027, and it will also be LTS.
- Node 24: still in LTS; end of life in April 2028.
The practical consequence: you’ll no longer have to memorize which versions “count”. Every release from here on is production-grade, and one release per year means fewer migrations per decade for your team. If you maintain libraries, the part that concerns you is the alpha channel: six months to test breaking changes before each release ships.
High impact
1. The internal _stream_* modules are gone
The removal with the most shrapnel, because it doesn’t break your code: it breaks old dependencies. The internal modules _stream_readable, _stream_writable, _stream_duplex, _stream_transform, _stream_passthrough and _stream_wrap — deprecated since Node 12 — have been removed entirely. Any require('_stream_readable') now throws MODULE_NOT_FOUND.
No package maintained in recent years does that, but ecosystems drag fossils along. To find out whether you have one:
grep -rn "_stream_" node_modules --include="*.js" -l | head
If something shows up, you have two exits: update that package to a version using the public API (node:stream), or — if it’s abandoned — replace it. The readable-stream polyfill covers most cases where the package is yours and you just need a mechanical migration.
2. Undici 8: the global fetch gets strict
Node 26 ships Undici 8.0, the implementation behind the global fetch. It brings stricter header validation and changes to redirect handling. If your code builds requests with dynamically generated headers — from a database, from user input, from another API — what used to pass silently can now throw a TypeError.
The pattern that shows up most in practice:
// If any value carries \n or \r (e.g. a token copied with a trailing newline),
// Undici 8 rejects it instead of silently sanitizing.
const res = await fetch(url, {
headers: { Authorization: `Bearer ${token}` },
});
The right fix is not catching the error: it’s sanitizing the value at the edge, where it enters your system (token.trim() when you read it). Having the request blow up before it leaves is the good behavior — with Undici 7 that token with a \n would travel and fail at the server on the other side, which is far harder to debug.
3. Native addons: mandatory recompilation
NODE_MODULE_VERSION moves to 147, so every compiled addon (bcrypt, sharp, better-sqlite3, canvas…) needs to be rebuilt or ship prebuilds for the new ABI. The error is unmistakable:
Error: The module was compiled against a different Node.js version using NODE_MODULE_VERSION 137.
npm rebuild solves it locally; in CI and Docker, rebuilding the image on the new base is enough. The detail that can actually stall you: the build requirements went up — GCC 13.2 minimum, and Python 3.9 support was dropped from the build toolchain. If you compile addons inside an old Debian/Alpine image, update the image before fighting the wrong error.
Medium impact
4. module.register() is runtime-deprecated
The async module-hooks API — the one used by instrumentation loaders, on-the-fly transpilation and APM agents — now emits a runtime deprecation. The designated replacement is the synchronous module.registerHooks() API.
This reaches you mostly through third parties: observability agents (Datadog, New Relic, OpenTelemetry) and tools like tsx live in that layer. The action item isn’t rewriting anything of yours: it’s updating those dependencies to the versions that already migrated before moving production to 26.
5. writeHeader() removed from the HTTP server
http.Server.prototype.writeHeader() was an undocumented alias of writeHead(). It’s gone. If it appears in your code, the fix is literal, letter for letter:
// Before
res.writeHeader(200, { 'Content-Type': 'application/json' });
// Now
res.writeHead(200, { 'Content-Type': 'application/json' });
A grep -rn "writeHeader" src/ tells you in seconds whether you’re affected. Frameworks (Express, Fastify, Nest) have always used the documented API; this bites hand-written HTTP code from many years ago.
6. type: "module" packages: the extensionless-CJS exception is gone
In packages declared as ESM, loading a CommonJS file without an extension relied on a historical resolver exception that Node 26 removes. If a "type": "module" package of yours had a require of a path without an explicit .cjs/.js, it now fails. The fix is adding the extension — which is what ESM resolution always asked for.
Low impact
localStoragewith no backing file returnsundefinedinstead of throwing. If you probed availability with try/catch aroundlocalStorage, invert the check: it’s a plainifnow.QuotaExceededErrorbecomes aDOMException-derived interface. Only affects code comparing the error by constructor or numericcode.- Several crypto APIs advance in the deprecation cycle — one reaches end of life and others start warning at runtime. If you see no warnings with
--pending-deprecationtoday, you’re not affected. assertaccepts printf-style messages in assertion errors. It’s an addition, but it changes text output: if your CI parses assert messages (bad idea, while you’re here), review it.
What does NOT break, even if you’ve read otherwise
- The Temporal API doesn’t break
Date. Temporal arrives enabled by default in Node 26, but it’s an addition: all yourDatecode keeps working exactly as (badly as) before. Migrating is optional and gradual — I have a full guide to Temporal in Node 26 with the conversion table from date-fns and dayjs. - Running TypeScript directly is now stable.
node app.tsworks with no flags: type stripping left experimental status. The only thing that “breaks” is the--experimental-transform-typesflag, which was removed precisely because the feature graduated — if you had it in an npm script, delete it and move on. - V8 14.6 only adds.
Map.prototype.getOrInsert(),Iterator.concat()and the rest of the Chromium 146 features are additions with zero migration cost.
The upgrade, in practice
The order that produces the fewest surprises:
- Install 26 next to your current version, not over it. With fnm or Volta it’s one command, and rolling back takes seconds.
npm rebuild(or deletenode_modulesand reinstall) to regenerate native addons against ABI 147.- Run the suite with deprecations visible:
Thenode --pending-deprecation --trace-deprecation ./node_modules/.bin/vitest runmodule.register()and crypto ones show up here, with a stack trace to the culprit. grepfor the fossils:_stream_,writeHeader(. Two minutes that save you a production surprise.- Update your agents (APM, telemetry, loaders) to versions that declare Node 26 support — it’s the layer that takes longest.
- Review the
enginesfield in yourpackage.jsonand your critical dependencies:npm ls --depth=0plus a glance atEBADENGINEwarnings during install.
When should you upgrade?
New project: start on 26 today. There’s no reason to begin something that will live for years on 24.
Production on 24: the sensible window is October 2026, when 26 enters LTS — with 24 supported until April 2028 there’s no rush, but no reason to wait until the end either: the longer you wait, the further this list drifts from fresh memory.
Production on 22: move now. April 2027 looks far away until it doesn’t, and the 22 → 26 jump is bigger than 24 → 26.
Further reading:
- The Temporal API in Node 26 — the big feature of this release, with migration from date-fns and dayjs.
- nvm vs fnm vs Volta — the comfortable way to keep 24 and 26 side by side while you migrate.
- The Complete Docker Guide for 2026 — test the upgrade in a container before touching anything.