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
Datethat needs no flag or polyfill. - V8 14.6 (Chromium 146), which adds
Map.prototype.getOrInsert(),getOrInsertComputed()andIterator.concat(). - Undici 8 as the HTTP client behind the built-in
fetch. - Native ABI bump:
NODE_MODULE_VERSIONis now 147, so native addons have to be rebuilt. - Removals and deprecations: the legacy
_stream_*modules,writeHeader(),--experimental-transform-types, and a runtime deprecation ofmodule.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 | ||
| V8 | 13.6 | 14.6 |
| Undici | 7.x | 8.x |
| NODE_MODULE_VERSION | 137 | 147 |
| Temporal | Not available | On by default |
| Support ends | April 2028 | April 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
localStoragenow returnsundefinedwhen no storage file is configured, so it no longer throws.QuotaExceededErroris now aDOMException-derived interface.util.inspectlabels 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
erasableSyntaxOnlyso 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,tsxor 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-deprecationtest 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.





