~/blog

Node 26 is LTS: what breaks when you move off Node 24

published

#node#lts#upgrade

TL;DR

Node.js 26.13.0 entered Long Term Support on October 28, 2026 under the codename Lithium, with updates promised through April 2029. Node 24 moved to Maintenance a week earlier and still has until April 2028, so nothing is on fire. But an upgrade from 24 is not a no-op: 26 deletes http.Server.prototype.writeHeader(), the six _stream_* internal modules and the --experimental-transform-types flag, turns module.register() and a handful of crypto patterns into runtime warnings, bumps NODE_MODULE_VERSION so native addons need a rebuild, and ships Linux binaries that require libatomic.

The problem

Every October an even-numbered Node release becomes LTS and the previous one slides into Maintenance. This year the dates from the release schedule look like this:

LineCodenameActive LTSMaintenanceEnd of life
22Jod2024-10-292025-10-212027-04-30
24Krypton2025-10-282026-10-202028-04-30
26Lithium2026-10-282027-10-202029-04-30

Maintenance means security and critical fixes only, so most teams on 24 will move to 26 sometime in the next year. The 26.13.0 release post is just a metadata flip: it changes process.release to report LTS and includes no other changes from 26.12.1. Everything that can break you landed back in 26.0.0 in May, which is where the list below comes from.

What is gone

These were deprecated for a while and are now removed outright. They throw or fail to resolve; there is no flag that brings them back.

Removed in 26Error you getReplacement
http.Server.prototype.writeHeader()TypeError: res.writeHeader is not a functionres.writeHead(status, headers) (#60635)
_stream_readable, _stream_writable, _stream_duplex, _stream_transform, _stream_passthrough, _stream_wrapERR_UNKNOWN_BUILTIN_MODULEthe public node:stream exports (#60657)
--experimental-transform-typesnode: bad optionstrip-only TypeScript, or tsx for enums and parameter properties (#61803)

The stream one bites through dependencies more than through your own code. Old polyfill shims and a few stream utilities still do require('_stream_readable') because that worked for a decade. Nothing in 26 resolves it, so the failure surfaces at startup with a module name you never typed.

The TypeScript flag is the subtle one. Type stripping has been on by default since 22.18 and stable since 24.12, and 26 keeps that. What 26 removes is the transform mode that also rewrote enum, runtime namespace blocks, parameter properties and import aliases into JavaScript. Per the TypeScript docs, those constructs now fail with ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX on every Node 26, and the documented way to run them is a loader such as node --import=tsx file.ts. We covered the strip-only behaviour in Node runs TypeScript now, until you write an enum; the difference is that on 24 the flag was an escape hatch, and on 26 it is not.

Two more removals that only matter at build time: NODE_MODULE_VERSION moved to 147, so every native addon compiled against 24 needs a rebuild (or a fresh prebuilt binary), and the build itself now requires GCC 13.2 and drops Python 3.9.

What now warns at runtime

Runtime deprecations do not break anything today, but they print on first use and are the list of what the next major removes. From the deprecations page:

What is on by default now

The Linux surprise

The 26.13.0 notes repeat a line that has been easy to miss: since Node 25, the official Linux binaries are linked against libatomic. Teams that jump from 24 to 26 skip the release that introduced it. On a minimal container image the symptom is the binary refusing to start with a missing shared library, so add the package to the base image (libatomic1 on Debian and Ubuntu, libatomic on Alpine and the RPM distros) before the version bump, not after the first failed deploy.

What to do

Before changing the version, find the hard failures with three greps across your code and your lockfile’s dependencies:

grep -rn "writeHeader(" src
grep -rnE "_stream_(readable|writable|duplex|transform|passthrough|wrap)" src node_modules --include='*.js' -l
grep -rn "experimental-transform-types" package.json .npmrc Dockerfile .github

Then:

  1. Rebuild or reinstall every native addon against 26 (npm rebuild is enough when prebuilds exist for ABI 147).
  2. Run the test suite once with NODE_OPTIONS=--throw-deprecation so the new runtime deprecations fail loudly instead of scrolling past.
  3. Add libatomic to Linux images.
  4. Update the engines field. ">=24" stays honest if you support both lines; ">=26" once you rely on Temporal or getOrInsert without a polyfill.
  5. Keep the CI matrix at 24 and 26. Node 24 is in Maintenance, not dead, and dropping it the week 26 went LTS is premature for a library.

Caveats

References