Welcome to HowToShipIt — practical how-to guides for developers: code, AI tools, and servers, explained step by step.

How to Upgrade Node 24 to 26 Without Breaking Anything (2026 LTS Checklist)

Node.js 26 is entering Active LTS this October, and Node 24 is about to drop into maintenance mode. That combination makes this the moment to plan your upgrade: if you move now, you do it on your own terms; if you wait, you end up scrambling when Node 24 stops getting routine fixes.

The good news: moving from Node 24 to Node 26 is one of the gentlest major upgrades in recent memory. The bad news: “gentle” still includes a handful of hard removals that will crash your app at startup if you touch them. This checklist walks you through exactly what breaks, how to find it in your codebase in minutes, and how to roll the new runtime out safely.

Last updated: 6 October 2026. All breaking changes below are taken from the official Node.js 26.0.0 release notes.

Why you should upgrade Node 24 to 26 now

Three dates explain the urgency. Node.js 26 shipped as a Current release on 5 May 2026 and is promoted to Active LTS in October 2026. Node 24 moves from Active LTS to maintenance on 20 October 2026, meaning from then on it gets only critical and security fixes. Node 24’s end of life is scheduled for 30 April 2028.

Maintenance mode is not a crisis, but it is a countdown. New features, performance work, and non-critical fixes stop flowing to your runtime. Starting the upgrade now — while Node 24 still receives full support — means you can run both versions side by side and compare behaviour with a safety net. You also get the real perks of 26: the Temporal API enabled by default, V8 14.6, Undici 8, and stable type stripping so node app.ts just works.

What actually breaks when you upgrade Node 24 to 26

The official 26.0.0 release notes list the SEMVER-MAJOR changes. For a typical app, only a few matter. Here they are, in order of how likely they are to bite you.

1. http.Server.prototype.writeHeader() is gone

The legacy writeHeader() alias has been moved to end-of-life and fully removed. If your code or any dependency calls res.writeHeader(200, headers), it will throw TypeError: res.writeHeader is not a function at runtime. The fix is a rename: use res.writeHead(200, headers). This is the highest-risk item because it fails at request time, not at startup, so a passing boot does not mean you are clean.

2. The legacy _stream_* internals are removed

The internal modules _stream_wrap, _stream_readable, _stream_writable, _stream_duplex, _stream_transform, and _stream_passthrough are now fully removed. They were never public API, but very old dependencies sometimes reached for them anyway. A require('_stream_readable') anywhere in your dependency tree now throws MODULE_NOT_FOUND. If you find a hit, the fix is almost always “upgrade that dependency” rather than editing its code.

3. crypto.createCipher() / createDecipher() reach end of life

DEP0182 has been moved to end-of-life, which removes crypto.createCipher() and crypto.createDecipher() (the insecure variants without an IV). If your code uses them, Node 26 refuses. The correct replacements have existed for years: crypto.createCipheriv() and crypto.createDecipheriv() with a random IV. If you grep and find createCipheriv, you are already fine.

4. Native addons must be rebuilt (ABI 147)

NODE_MODULE_VERSION jumps to 147, so any compiled C++ addon (think bcrypt, sharp, sqlite3, or your APM agent’s native bits) built against Node 24 will not load on 26. The error looks like Error: The module was compiled against a different Node.js version. The fix is npm rebuild, or deleting node_modules and reinstalling from scratch. Do this before anything else in your test cycle — it is the single most common reason an upgrade “mysteriously” fails.

5. module.register() is now runtime-deprecated

module.register() — the API used to install module customisation hooks — is runtime-deprecated in 26. Your code still works, but Node prints a deprecation warning. If your loaders are built on it, start migrating; it is the obvious candidate for removal in a later major. Run with --trace-deprecation (shown below) to find exactly who calls it.

6. The --experimental-transform-types flag is removed

Type stripping graduated from experimental to stable, so the flag was deleted. If you have --experimental-transform-types in any npm script, Dockerfile CMD, or Procfile, Node 26 refuses to start with a bad-option error. Just delete the flag — node app.ts works with no flags now.

7. Smaller gotchas worth one line each

  • Undici 8 has stricter header validation in fetch: if your code (or a proxy) set malformed headers that Undici 7 tolerated, requests can now fail. Your HTTP client tests will catch this.
  • Extensionless CJS resolution is gone for type: module packages: an import './utils' inside a type: "module" package no longer resolves without the .js extension. Relative imports now need explicit extensions.
  • assert accepts printf-style messages: assertion failure text can change shape. Only matters if your CI parses assert output (do not).
  • Build requirements tightened: compiling Node from source now needs GCC 13.2+ and Python 3.10+ (3.9 support dropped). Irrelevant if you use official prebuilt images, which is what you should be doing.

What does NOT break (despite the panic posts)

A few things people worry about that are safe to ignore:

  • Temporal does not break Date. The Temporal API ships enabled by default in Node 26, but it is purely additive — all your existing Date code behaves exactly as before. Migrating to Temporal is optional and can happen file by file.
  • V8 14.6 is additive. New built-ins like Map.prototype.getOrInsert() and Iterator.concat() cost you zero migration work.
  • You do not need to rewrite your modules. This upgrade is checklist work, not refactoring work.

The Node 24 to 26 upgrade checklist

Work through this in order. For most apps it takes an afternoon, not a sprint.

Step 1: Find every place that picks a Node version

Before touching code, inventory where the version is decided — local, CI, and production are often three different answers:

node --version
rg 'node-version|FROM node|"node"' .github Dockerfile package.json .nvmrc .tool-versions 2>/dev/null

Then pin one explicit version instead of relying on whatever a machine happens to have. Your package.json communicates compatibility; your .nvmrc, Docker tag, or hosting settings select the version:

// package.json
{
  "engines": {
    "node": ">=26 <27"
  }
}
# .nvmrc
26

Step 2: Grep for the fossils

Five searches find virtually every real breakage. Run them at the repo root:

rg 'writeHeader\(' --type js --type ts
rg "require\(['\"]_stream" 
rg 'module\.register\('
rg 'createCipher\(|createDecipher\('
rg 'experimental-transform-types'

Any hit on the first four is a must-fix before the bump. The fifth is a delete-the-flag fix. The writeHeader( search deserves care: writeHead is fine, only writeHeader is removed — make sure your regex does not conflate the two (the one above does not, since writeHead( will not match writeHeader\().

Step 3: Install 26 next to your current version

Do not overwrite your working Node 24. With a version manager, rollback takes seconds:

fnm install 26
fnm use 26

Then rebuild native modules and reinstall dependencies from scratch:

rm -rf node_modules package-lock.json
npm install
npm rebuild

Step 4: Run the test suite with deprecations visible

This is where module.register(), the crypto deprecations (DEP0203, DEP0204), and the stream deprecation (DEP0201) reveal themselves, with stack traces pointing at the culprit:

node --pending-deprecation --trace-deprecation ./node_modules/.bin/vitest run

Run your HTTP client tests too — Undici 8’s stricter header validation shows up there. Also test background workers, cron jobs, and queue consumers, not only the web server. Warnings that appear under 26 and did not appear under 24 are your fix list.

Step 5: Update the type definitions and CI

npm install -D @types/node@^26

In CI, test both versions in parallel before changing production. This catches incompatibilities while Node 24 is still your safe default:

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
  - run: npm run build

If 26 fails while 24 passes, that failure is useful information, not a production incident.

Step 6: Rebuild your Docker image from scratch on Node 26

Change the base image in a branch — never float on node:latest, or a routine rebuild silently becomes a runtime migration:

FROM node:26-bookworm-slim AS runner

Rebuild with --no-cache, confirm native packages load (a startup log line helps), and verify the image’s reported versions:

docker run --rm myapp:node26 node --version && docker run --rm myapp:node26 npm --version

Step 7: Roll out service by service

A one-week rollout plan that fits most teams:

  1. Day 1: Node 26 runs in CI alongside Node 24. Collect every deprecation warning.
  2. Day 2: Run the greps, fix your own code, open issues or upgrade for dependencies.
  3. Day 3: Rebuild a Docker image from scratch on Node 26 and confirm native packages load.
  4. Day 4: Deploy to one low-risk service, or a single instance behind your load balancer. Compare error rates and memory against the Node 24 instances for 24 hours.
  5. Day 5 onward: Roll the rest out service by service. Keep the Node 24 image tagged so rollback is a one-line change.

During the canary, watch: request error rate by runtime version, p99 latency, restart rate, out-of-memory events, event-loop delay, outbound TLS failures, and native addon errors. Keep your existing SLOs as the contract — do not loosen them to make the candidate look healthy.

Step 8: Update your agents and telemetry

APM, error-tracking, and telemetry loaders are the layer that takes longest to declare Node 26 support. Check your vendors’ compatibility pages and upgrade those agents first — a broken APM loader at startup is a classic upgrade-day surprise.

Should you wait for Node 27 instead?

Probably not. Node 27 is the first release on the new annual cadence, and waiting for it means sitting on a maintenance-mode Node 24 longer than you need to. Node 26 is also the last release under the old even/odd LTS rhythm, which makes it the natural landing spot. Move to 26 now, treat 27 as next year’s job.

Further Reading & References

Leave a Comment