Node.js 26 became the active LTS line in late October 2026. Version 26.13.0, codenamed "Lithium", is supported until April 2029, and platforms such as Vercel already use it as the default for new projects. If you run production apps on Node 24, this guide covers what changes in Node.js 26 LTS, what will break, and the checklist I follow to upgrade a real MERN or Next.js codebase without surprises.

The LTS promotion added no new features. Everything listed here first shipped in 26.0.0 in May 2026. The difference now is that this release line is the one you're expected to run in production.

Node.js 26 LTS at a glance

Compared with Node 24, the main changes are:

  • Temporal API enabled by default: a modern replacement for Date that needs no flag or polyfill.
  • V8 14.6 (Chromium 146), which adds Map.prototype.getOrInsert(), getOrInsertComputed() and Iterator.concat().
  • Undici 8 as the HTTP client behind the built-in fetch.
  • Native ABI bump: NODE_MODULE_VERSION is now 147, so native addons have to be rebuilt.
  • Removals and deprecations: the legacy _stream_* modules, writeHeader(), --experimental-transform-types, and a runtime deprecation of module.register().

Node 26 is also the last major release on the old six-month cadence. Starting with v27, Node.js moves to one major release per year. That makes 26 a good line to standardize on.

ComponentNode 24Node 26
V813.614.6
Undici7.x8.x
NODE_MODULE_VERSION137147
TemporalNot availableOn by default
Support endsApril 2028April 2029

Breaking changes in Node 26 that can hit your app

Most application code will run unchanged. Upgrades usually fail because of dependencies and build tooling. These are the semver-major changes from the official v26.0.0 release notes that matter in practice.

Legacy stream modules are gone

_stream_readable, _stream_writable, _stream_duplex, _stream_transform, _stream_passthrough and _stream_wrap have been removed. Your own code probably doesn't use them, but old packages sometimes do, often deep in the dependency tree. The fix is to import from node:stream:

// Before (breaks on Node 26)
const Readable = require('_stream_readable');

// After
const { Readable } = require('node:stream');

http.Server.prototype.writeHeader() is removed

This was an undocumented alias. Replace it with writeHead():

res.writeHead(200, { 'Content-Type': 'application/json' });

Native addons need a rebuild

The ABI changed from 137 to 147. A node_modules folder built on Node 24 that includes bcrypt, sharp, better-sqlite3 or another native module will fail to load. Reinstall dependencies on Node 26 itself, and don't copy node_modules between Docker stages that use different Node versions. If you build from source, GCC 13.2 is now the minimum and Python 3.9 is no longer supported.

Smaller behavior changes

  • localStorage now returns undefined when no storage file is configured, so it no longer throws.
  • QuotaExceededError is now a DOMException-derived interface.
  • util.inspect labels proxied objects, which can break snapshot tests that compare inspected output.
  • HTTP upgrade requests that carry a body are handled differently. If you run WebSocket servers, test them.

Step-by-step: upgrade to Node.js 26 LTS

This is the order I use. It surfaces problems before you deploy them.

1. Switch locally and pin the version

nvm install 26
nvm use 26
node -v   # v26.13.x or later
echo "26" > .nvmrc

Then update engines in package.json so CI and hosting platforms use the same major version:

{
  "engines": {
    "node": "26.x"
  }
}

2. Do a clean install

rm -rf node_modules
npm ci

A clean install rebuilds native addons against the new ABI. If a package fails to compile, check whether a newer version publishes prebuilt binaries for Node 26 before you try to fix the toolchain.

3. Search your dependencies for removed APIs

grep -rlE "require\(['\"]_stream_|from ['\"]_stream_" node_modules --include=*.js | head
grep -rl "writeHeader(" node_modules --include=*.js | head

Any match is a package you need to upgrade or replace. Run npm ls <package> to see which top-level dependency pulls it in.

4. Run tests with deprecations turned into errors

NODE_OPTIONS="--throw-deprecation" npm test

This flag makes every runtime deprecation fail loudly, so you find out now about APIs that will disappear in Node 27 or 28.

5. Add Node 26 to the CI matrix before dropping Node 24

# .github/workflows/ci.yml
strategy:
  matrix:
    node-version: [24, 26]
steps:
  - uses: actions/checkout@v4
  - uses: actions/setup-node@v4
    with:
      node-version: ${{ matrix.node-version }}
      cache: npm
  - run: npm ci
  - run: npm test

Once Node 26 passes consistently, remove 24 from the matrix.

Handling deprecation warnings (DEP0205 and friends)

The deprecation you're most likely to see on Node 26 is this one:

[DEP0205] DeprecationWarning: module.register() is deprecated.
Use module.registerHooks() instead.

module.register() runs async loader hooks on a separate thread, which is hard to coordinate. Its replacement, module.registerHooks(), is synchronous, runs in the same thread, and works for both CommonJS and ESM. See the DEP0205 deprecation entry for details. If you wrote your own loader, migrate it like this:

import { registerHooks } from 'node:module';

registerHooks({
  resolve(specifier, context, nextResolve) {
    if (specifier.startsWith('@app/')) {
      specifier = new URL(specifier.slice(5) + '.js', import.meta.url + '/../src/').href;
    }
    return nextResolve(specifier, context);
  },
});

Often the warning comes from a dependency rather than your own code. APM and monitoring SDKs, and some dev servers, still call module.register(). Find the source with:

node --trace-deprecation server.js

If the stack trace points into node_modules, upgrade that package or wait for its fix. It's only a warning, so your app keeps working in the meantime. Node 26 also adds runtime deprecations for two crypto APIs (DEP0203 and DEP0204) and one stream API (DEP0201). The same --trace-deprecation run will show whether any of them affect you.

TypeScript projects: type stripping without --experimental-transform-types

Node 26 still runs .ts files directly by stripping type annotations. What's gone is --experimental-transform-types, the flag that also transformed TypeScript-only syntax such as enum, namespace and parameter properties. If your npm scripts use that flag, they will fail at startup.

You have two options:

  • Write erasable-only TypeScript. Turn on erasableSyntaxOnly so the compiler flags any syntax that Node can't simply strip:
{
  "compilerOptions": {
    "erasableSyntaxOnly": true,
    "verbatimModuleSyntax": true,
    "module": "nodenext"
  }
}

Replace enums with as const objects:

export const Role = {
  Admin: 'admin',
  User: 'user',
} as const;
export type Role = (typeof Role)[keyof typeof Role];
  • Keep a build step using tsc, tsx or esbuild if you rely on decorators or enums, as many NestJS-style codebases do.

New features worth using: Temporal API

This is the feature that justifies the upgrade even if nothing else does. Date is mutable, has only partial time zone support, and counts months from zero. Temporal fixes all three, and in Node.js 26 LTS it's available as a global with no import.

// Current time in a specific time zone
const dhaka = Temporal.Now.zonedDateTimeISO('Asia/Dhaka');
console.log(dhaka.toString());
// 2026-11-06T14:32:10.123+06:00[Asia/Dhaka]

// Immutable arithmetic: add() returns a new value
const otpExpiresAt = Temporal.Now.instant().add({ minutes: 5 });

// Calendar math without manual millisecond conversions
const today = Temporal.Now.plainDateISO();
const daysLeft = today.until(Temporal.PlainDate.from('2026-12-31')).days;

// Compare safely
if (Temporal.Instant.compare(Temporal.Now.instant(), otpExpiresAt) > 0) {
  throw new Error('OTP expired');
}

MongoDB drivers and most ORMs still expect Date, so convert at the persistence boundary:

// Temporal -> Date (for storage)
const asDate = new Date(otpExpiresAt.epochMilliseconds);

// Date -> Temporal (after reading)
const instant = Temporal.Instant.fromEpochMilliseconds(doc.expiresAt.getTime());

Expiry logic like the example above is exactly what I covered in Redis vs Database for OTP Storage. Temporal makes the time comparisons in that kind of code much harder to get wrong.

Map.getOrInsert and Iterator.concat

V8 14.6 ships the "upsert" proposal. It replaces the if (!map.has(k)) map.set(k, ...) pattern you've probably written many times:

// Before
if (!groups.has(user.role)) groups.set(user.role, []);
groups.get(user.role).push(user);

// Node 26
groups.getOrInsert(user.role, []).push(user);

// Lazily computed default (callback only runs on a miss)
const client = clients.getOrInsertComputed(tenantId, (id) => createDbClient(id));

The same methods exist on WeakMap, which is handy for per-request caches. A simple in-memory rate limiter is a good fit. If you need one that works across instances, see how to add rate limiting to a Node.js API with Redis.

Iterator.concat() chains several iterables lazily, without building an intermediate array:

for (const job of Iterator.concat(urgentJobs, normalJobs.values())) {
  await run(job);
}

Deploying Node 26 on Vercel and Docker

On Vercel, setting engines.node to 26.x is enough. If your range already allows 26 (for example >=20), your next deploy picks it up automatically. According to Vercel's changelog, fetch in Node 26 Functions can reuse HTTP/2 connections. If an upstream API misbehaves over HTTP/2, set VERCEL_FETCH_DISABLE_H2=1 to force HTTP/1.1. Prebuilt deployments need Vercel CLI 65.8.0 or later.

For Docker, install dependencies inside a Node 26 image so native modules are built against ABI 147:

FROM node:26-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev

FROM node:26-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY --from=deps /app/node_modules ./node_modules
COPY . .
USER node
CMD ["node", "server.js"]

Both stages use the same major version. That's the important part.

Should you upgrade now?

For most teams, yes. Node 24 moves into maintenance as Node 26 takes over active LTS, and Node 26 gets security fixes until April 2029. Here's how I'd decide:

  • New projects: start on Node 26.
  • Apps with few native dependencies: upgrade now. It's usually a version bump plus a clean install.
  • Apps that depend on APM agents, custom loaders or older stream libraries: run the dependency search and the --throw-deprecation test pass first, then upgrade in the next sprint.
  • Still on Node 22: it reaches end of life in April 2027, so plan to move directly to 26.

If you're newer to the runtime and want background on why the event loop, V8 and libuv matter here, start with What is Node.js?. Otherwise, switch a branch to Node 26, run npm ci and the test suite with deprecations as errors, and you'll know within an hour how much work the upgrade involves.