DevLazar
Note
069
Series
The Wire · Tooling
Date
Nov 10, 2026
Read
7 min · 1,470 words
Rev
1
Drawn by
L. Stanković · Niš
Lab notes / The Wire

Node.js 26 'Lithium' Goes LTS: Temporal Ships, and the Deprecation Bills Come Due

Node.js 26.13.0 'Lithium' became Active LTS on October 28, with support to April 2029. Temporal by default, V8 14.6, real removals, and what upgrading a NestJS or Express service actually takes.

Lazar StankovićEngineer, DevLazar · Niš, Serbia

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:

Listing 1JavaScript
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:

Listing 2Diff
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.

Listing 3Diff
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:

Listing 4JavaScript
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.

Listing 5JavaScript
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:

Listing 6Dockerfile
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.

Column chart of days of upstream support remaining from November 10, 2026: Node 22 has 171, Node 24 has 537 and Node 26 has 902
Chart: Upstream support left from Nov 10, 2026, in days. Source: Node.js Release WG schedule.
  1. Set "engines": { "node": "^26.13.0" } in package.json and write 26 to .nvmrc.
  2. Move base images to node:26-slim, or pin the patch with node:26.13.0-slim.
  3. Run CI on Node 24 and 26; keep 22 in the matrix only for published packages, and only until April 30, 2027.
  4. Grep your code and node_modules for _stream_, writeHeader, module.register( and GCM decryption without authTagLength.
  5. Treat DEP0201, DEP0203, DEP0204 and DEP0205 warnings in test output as failures, not noise.
  6. Keep the tsc or SWC build for Nest; adopt erasableSyntaxOnly only for scripts you want to run as .ts.

A minimal service matrix for item 3, in .github/workflows/ci.yml:

Listing 7YAML
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.

Lazar Stanković
Engineer and writer, running a one-man shop in Niš, Serbia. I build systems that survive their launch and write down what the work teaches.
Related notes
Note 068 · The Wire · 8 min
NestJS 12: ESM Packages, Schema-First Validation, and an Upgrade You Can Mostly Script
NestJS 12 shipped August 27 with ESM-only packages, Standard Schema validation on route decorators, Vitest and oxlint defaults, and a hosted observability SDK. What breaks, what does not, and how to move a v11 app.
Note 067 · The Wire · 8 min
.NET 11 and C# 15 Ship: Unions, In-Box Zstandard, and a Support Clock That Stops With .NET 10's
.NET 11 went GA on November 10 with C# 15 union types, Zstandard in System.IO.Compression and opt-in runtime async. It is an STS release whose support ends the same day as .NET 10 LTS.
Note 072 · The Wire · 16 min
Vercel, Netlify, Cloudflare, Coolify, Dokploy: The Limits, the Meters and the Monthly Bill
CPU caps, memory ceilings, build limits and egress rates for five deploy targets, from their own docs as of November 11, 2026, plus two worked monthly bills. The meters differ more than the sticker prices.
← Note 068 · NestJS 12: ESM Packages, Schema-First Validation, and an Upgrade You Can Mostly ScriptNote 070 · CoreWeave Beats, Raises, and Pays $846 Million in Interest →
The Wire · Weekly · Free
Notes like this one, every week.

AI infrastructure, shipping on a deadline, and the craft of building things that last. No noise.