On October 28, Node.js 26.13.0 marked the transition of the 26.x line into Long Term Support under the codename "Lithium", with updates promised "through to the end of April 2029." The promotion changed nothing but metadata such as process.release; the substance arrived when 26.0.0 shipped as Current on May 5, a few days past the April slot the schedule had pencilled in. That release turned on Temporal by default, moved to V8 14.6, and finally removed APIs that have been printing warnings for years. It is also the last line cut under the old odd/even model, which makes it the LTS most teams will live on while the new annual cadence beds in.
Temporal is a global now, and Date can start packing
The headline commit is five words long: build: enable Temporal by default. No flag, no import. Temporal splits what Date blurred into separate types, Instant, ZonedDateTime, PlainDate, PlainTime, Duration and the rest, and its design includes DST-safe arithmetic, which is the part your billing cron will quietly appreciate. The same engine bump, V8 14.6.202.33 from Chromium 146, adds Map.prototype.getOrInsert(), getOrInsertComputed() and Iterator.concat(). The snippet covers calendar math, a day versus 24 hours across a DST change, and both V8 additions:
1// scripts/dates.mjs: Temporal is a global in Node.js 26, no flag, no import 2const released = Temporal.PlainDate.from('2026-05-05'); 3const lts = Temporal.PlainDate.from('2026-10-28'); 4 5released.until(lts).toString(); // 'P176D' 6released.until(lts, { largestUnit: 'months' }).toString(); // 'P5M23D' 7 8// EU clocks jump forward on 2026-03-29 9const standup = Temporal.ZonedDateTime.from({ 10 timeZone: 'Europe/Belgrade', year: 2026, month: 3, day: 28, hour: 9, 11}); 12standup.add({ days: 1 }).toString(); // '2026-03-29T09:00:00+02:00[Europe/Belgrade]' 13standup.add({ hours: 24 }).toString(); // '2026-03-29T10:00:00+02:00[Europe/Belgrade]' 14 15// V8 14.6: Map upsert and iterator sequencing 16const jobsByTenant = new Map(); 17jobsByTenant.getOrInsertComputed('acme', () => []).push('job-1'); 18console.log([...Iterator.concat([1, 2], new Set([3, 4]))]); // [1, 2, 3, 4]
One boundary to respect: MDN still labels Temporal limited availability, not Baseline, because some major browsers lack it. Use it freely in server code; in shared Next.js modules that also reach the client, keep a polyfill or a globalThis.Temporal guard.
The fetch story is quieter. Node 26 bundles undici 8.0.2, and InfoQ reports that fetch() needs no API changes. The Node notes describe no behavior changes, so treat it like any new major of your HTTP client: replay real outbound traffic, retries and connection pools included, through staging first.
TypeScript runs natively, Nest still compiles
Type stripping has been stable since v25.2.0 and v24.12.0, and 26.0.0 removed --experimental-transform-types, the escape hatch that let Node rewrite TypeScript-only syntax. Now enum, namespaces with runtime code, parameter properties and import aliases simply error, and a runtime namespace throws ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX. Node ignores tsconfig.json entirely, so paths aliases fail too. The docs recommend TypeScript 5.8 or newer with erasableSyntaxOnly, verbatimModuleSyntax and rewriteRelativeImportExtensions. An enum becomes a const object:
1 // src/roles.ts 2-export enum Role { 3- Admin = 'admin', 4- Member = 'member', 5-} 6+export const Role = { 7+ Admin: 'admin', 8+ Member: 'member', 9+} as const; 10+export type Role = (typeof Role)[keyof typeof Role];
Here is the catch for Nest shops. The same page says decorators are not transformed and produce a parser error, and Nest's idiomatic constructor(private readonly users: UsersService) is a parameter property. A Nest service keeps its tsc or SWC build and runs dist/main.js, as before. Type stripping earns its keep in seed scripts, migrations and one-off tooling, where node scripts/seed.ts now works, provided imports carry their .ts extensions and type-only imports say import type.
What 26 removes, and what it starts warning about
The removals are old debts. writeHeader() is gone in favor of writeHead(); the release notes say http.Server.prototype, while the deprecation index files it as DEP0063 under ServerResponse.prototype, the object your handlers actually call. The internal _stream_readable, _stream_writable and siblings reached End-of-Life as DEP0193, and _stream_wrap followed as DEP0125, runtime-deprecated since v12.0.0. A dependency that still reaches for these was abandoned some time ago and has only now been told.
1 // src/legacy-server.js 2 const http = require('node:http'); 3-const Readable = require('_stream_readable'); 4-const Transform = require('_stream_transform'); 5+const { Readable, Transform } = require('node:stream'); 6 7 http.createServer((req, res) => { 8- res.writeHeader(200, { 'content-type': 'text/plain' }); 9+ res.writeHead(200, { 'content-type': 'text/plain' }); 10 res.end('ok'); 11 }).listen(3000);
The deprecation that will touch the most tooling is DEP0205: module.register() is now runtime-deprecated in favor of module.registerHooks(), which runs synchronous hooks on the main thread and works for every module type. The docs say the off-thread async design caused problems that cannot be resolved, and ask you to migrate "as soon as possible." APM agents, mocking libraries and custom loaders are the usual suspects. A minimal preload file, before and after:
1// register.mjs, preloaded with: node --import ./register.mjs dist/main.js 2// Before (runtime-deprecated in Node.js 26 as DEP0205): 3// import { register } from 'node:module'; 4// register('./hooks.mjs', import.meta.url); 5import { registerHooks } from 'node:module'; 6 7registerHooks({ 8 resolve(specifier, context, nextResolve) { 9 if (specifier === 'virtual:config') { 10 return { url: 'virtual:config', shortCircuit: true }; 11 } 12 return nextResolve(specifier, context); 13 }, 14 load(url, context, nextLoad) { 15 if (url === 'virtual:config') { 16 return { format: 'module', source: 'export default { port: 3000 };', shortCircuit: true }; 17 } 18 return nextLoad(url, context); 19 }, 20});
Crypto and streams got stricter as well. DEP0182 is End-of-Life: GCM decryption with a tag shorter than the 16-byte default now requires an explicit authTagLength, in line with NIST SP 800-38D. Passing a CryptoKey into node:crypto functions such as createHmac() or sign() is now a runtime deprecation (DEP0203), as is KeyObject.from() with a non-extractable key (DEP0204), and Duplex.toWeb() wants readableType instead of type (DEP0201). Note the constant below: derive the tag length from your protocol, never from the tag an attacker hands you.
1// src/crypto-compat.js 2const crypto = require('node:crypto'); 3const { Duplex } = require('node:stream'); 4 5const TAG_LENGTH = 12; // what your protocol actually uses; the AES-GCM default is 16 6 7function decrypt(key, iv, ciphertext, tag) { 8 // Before: createDecipheriv('aes-256-gcm', key, iv) accepted a short tag (DEP0182, now EOL) 9 const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv, { authTagLength: TAG_LENGTH }); 10 decipher.setAuthTag(tag); 11 return Buffer.concat([decipher.update(ciphertext), decipher.final()]); 12} 13 14function toWebPair(duplex) { 15 // Before: Duplex.toWeb(duplex, { type: 'bytes' }) (DEP0201, runtime warning in 26) 16 return Duplex.toWeb(duplex, { readableType: 'bytes' }); 17} 18 19module.exports = { decrypt, toWebPair };
The 26.0.0 notes also carry quieter semver-major changes, including localStorage returning undefined without a backing file and a fix to HTTP upgrades that carry a body; hand-rolled WebSocket upgrade code deserves a test.
Addons, toolchains and the Linux binary
NODE_MODULE_VERSION is now 147, with 146 reserved for Electron 42, so prebuilt native addons must be rebuilt. Reinstall dependencies inside the Node 26 image rather than copying node_modules across versions. Building Node itself now requires GCC 13.2, drops Python 3.9, targets Power 9 on AIX and IBM i, and adds cargo and rustc checks for Temporal, so custom builds now pull in a Rust toolchain.
One more trap sits in the LTS announcement: since Node 25, the official Linux binaries link against libatomic, which some distributions do not install by default. That matters most for teams jumping straight from 22 or 24. If you lay the official tarball onto a minimal Debian base, add apt-get install -y --no-install-recommends libatomic1 yourself. With the official image, a two-stage build for a Nest or Express service looks like this:
1FROM node:26-slim AS build 2WORKDIR /app 3COPY package*.json ./ 4RUN npm ci 5COPY . . 6RUN npm run build 7 8FROM node:26-slim 9WORKDIR /app 10ENV NODE_ENV=production 11COPY package*.json ./ 12RUN npm ci --omit=dev 13COPY --from=build /app/dist ./dist 14USER node 15CMD ["node", "dist/main.js"]
The calendar, and an upgrade checklist
The calendar sets the urgency. Per the Release Working Group schedule, Node 20 reached end of life on April 30, 2026. Node 22 has been in maintenance since October 21, 2025 and ends April 30, 2027. Node 24 entered maintenance on October 20, 2026 and ends April 30, 2028. Node 26 stays in Active LTS until October 20, 2027 and ends April 30, 2029. Counted from today, that leaves 171, 537 and 902 days of upstream support for 22, 24 and 26.

- Set
"engines": { "node": "^26.13.0" }inpackage.jsonand write26to.nvmrc. - Move base images to
node:26-slim, or pin the patch withnode:26.13.0-slim. - Run CI on Node 24 and 26; keep 22 in the matrix only for published packages, and only until April 30, 2027.
- Grep your code and
node_modulesfor_stream_,writeHeader,module.register(and GCM decryption withoutauthTagLength. - Treat DEP0201, DEP0203, DEP0204 and DEP0205 warnings in test output as failures, not noise.
- Keep the
tscor SWC build for Nest; adopterasableSyntaxOnlyonly for scripts you want to run as.ts.
A minimal service matrix for item 3, in .github/workflows/ci.yml:
1name: ci 2on: [push, pull_request] 3jobs: 4 test: 5 runs-on: ubuntu-latest 6 strategy: 7 matrix: 8 node: [24, 26] 9 steps: 10 - uses: actions/checkout@v4 11 - uses: actions/setup-node@v4 12 with: 13 node-version: ${{ matrix.node }} 14 cache: npm 15 - run: npm ci 16 - run: npm test
The 30,000-foot read
An LTS promotion is a ceremony, not a release. The real change happened in May, and the six months since were the project's way of letting the rest of us catch up. What sits within your control is small and mechanical: removed modules, a renamed option, a hook API, a crypto parameter, a Docker tag. What does not is how a new undici major behaves under your traffic and when Safari ships Temporal; for those, test and wait, respectively.
The larger shift is the calendar itself. Lithium is the last LTS chosen by parity. Node 27's first alpha was posted on October 29, and under the new schedule every major spends six months in Alpha, six as Current and 30 in LTS, so 27 itself becomes LTS in October 2027. One upgrade a year, on a 36-month horizon. The biennial chore becomes an annual habit, which is the better habit anyway: smaller jumps, fewer ancient warnings settled all at once.
Sources: Node.js 26.13.0 release notes (LTS transition on October 28, Lithium codename, support to end of April 2029, libatomic note); Node.js 26.0.0 release notes (May 5 release, Temporal by default, V8 14.6.202.33, undici 8.0.2, removals, NODE_MODULE_VERSION 147, toolchain and semver-major changes); Evolving the Node.js Release Schedule (26 as last old-model line, Alpha/Current/LTS cadence, planned April release); Node.js Release Working Group schedule (Active LTS, maintenance and end-of-life dates for 20 through 27; the GitHub README served via jsDelivr); Node.js Releases (first-release dates and status per line); Node.js release index (27.0.0 alpha posting date); Node.js deprecations, v26.13.0 docs (DEP0063, DEP0125, DEP0182, DEP0193, DEP0201, DEP0203, DEP0204, DEP0205 and replacements); Node.js TypeScript docs, v26.13.0 (type stripping status, unsupported syntax, decorators, tsconfig guidance); MDN Temporal reference (Temporal classes, limited availability); TC39 Temporal documentation (Temporal.Now, DST-safe arithmetic); InfoQ (secondary: undici needing no fetch API changes, addon rebuild). Correction: 26.0.0 shipped on May 5, 2026, not in April as the planned schedule said. The release notes and the deprecation index name different prototypes for the removed writeHeader(); both are given above. CAVEAT: undici 8's own changelog was not checked on a primary page, so no specific fetch behavior changes are claimed; the 26.0.0 notes state no OpenSSL version, and this post makes no OpenSSL claim; Docker Hub tags could not be checked directly and follow the official image's usual major-version naming; Temporal output comments were worked out by hand and cross-checked with Python's zoneinfo, not executed on a Node 26 binary; the registerHooks preload and GCM snippets were run on Node 22.23.4, which shares those APIs. Chart day counts are arithmetic from the official end-of-life dates.