Skip to main content

Guide

Node.js 20 to 22 Upgrade: Node 20 Hits EOL 2026

Node.js 20 reached EOL in April 2026. Use this Node 20 to 22 breaking-change checklist to decide whether to move to Node 22 first or jump to Node 24 LTS.

·PkgPulse Team·
0
Hero image for Node.js 20 to 22 Upgrade: Node 20 Hits EOL 2026

Quick answer: upgrade Node.js 20 now, but choose the target deliberately

If your production app is still on Node.js 20 in 2026, the urgent issue is not performance. It is support status. The Node.js release schedule lists Node.js 20 "Iron" as ending on 2026-04-30, which means the upstream project no longer ships security or bug-fix releases for that major line.

The safe answer for a Node.js 20 application is:

  • Upgrade immediately if the app serves public traffic or handles sensitive data. Node 20 is past upstream EOL.
  • Use Node.js 22 as the lowest-risk bridge when your hosting provider, native addons, or enterprise baseline are not ready for Node 24 yet.
  • Evaluate Node.js 24 as the current LTS target if you can test the slightly newer runtime now. For that decision, use the current Node 22 vs Node 24 guide after you understand the Node 20 to 22 migration surface.

Most healthy Node.js 20 apps can move to Node.js 22 with a version bump, dependency reinstall, CI update, and a focused test pass. The real migration work is usually in CI images, Dockerfiles, serverless runtime settings, native addon rebuilds, old URL APIs, and ESM/CommonJS edge cases.

Node 20 vs Node 22 support status in 2026

RuntimeCodename2026 statusUpstream end dateProduction guidance
Node.js 20IronEnd-of-life after April 20262026-04-30Move off this line unless you have paid external support.
Node.js 22JodMaintenance LTS2027-04-30Good conservative landing zone for Node 20 apps.
Node.js 24KryptonActive LTS during most of 20262028-04-30Better default for new work if your platform and dependencies pass tests.

That table is why this guide keeps the Node 20 to 22 path practical instead of pretending Node 22 is the only possible 2026 destination. Node 22 is still supported, stable, and much closer to Node 20 than Node 24. It is the right interim target when you need a conservative migration. But if you are doing a larger platform upgrade, compare Node 24 before locking in another runtime change.

Node 20 to 22 breaking changes checklist

Start here if you searched for node 20 to 22 breaking changes.

Area to checkWhat changed or can breakHow to test itTypical fix
Node version pins.nvmrc, .node-version, Docker, CI, and serverless settings may still point at 20.Run node --version locally, in CI, in staging, and in the final container.Pin 22 or a specific 22.x patch in every runtime surface.
Dependency installOptional/native packages may resolve different binaries for Node 22.Delete node_modules, reinstall, run lockfile and native-addon smoke tests.Rebuild native modules or update packages with Node 22 prebuilds.
Native addonsV8/ABI changes can expose old node-gyp, nan, SQLite, image, or crypto bindings.Run install on the same OS/CPU as production and exercise affected paths.Prefer maintained N-API/prebuild packages; run npm rebuild or equivalent.
CommonJS requiring ESMNode 22.12+ can synchronously require() compatible ESM graphs, but not modules with top-level await.Test CJS entrypoints that load ESM-only packages.Remove old dynamic-import hacks when tests prove safe; keep async import for top-level-await modules.
Deprecated URL parsingOlder url.parse() usage can emit deprecation warnings and obscure real production logs.Run tests with warnings surfaced, for example NODE_OPTIONS=--trace-warnings.Move new code to new URL() and URLSearchParams.
Experimental flagsFlags around test coverage, permissions, WebSocket, watch mode, or prior ESM experiments may be stale.Audit NODE_OPTIONS, npm scripts, CI commands, and process managers.Remove flags that are no longer needed; keep permission-model usage isolated and tested.
Test runner assumptionsnode:test is more capable in 22, but Jest/Vitest projects may rely on jsdom, fake timers, or custom mocks.Run the existing suite before changing test frameworks.Upgrade runtime first; compare node:test vs Vitest vs Jest later.
Platform supportHosted runtimes can lag upstream Node support or deprecate older versions on their own schedule.Check your provider's runtime matrix before changing production config.Use containers if managed runtime support lags, or choose the newest supported LTS.

For most teams, that checklist is more useful than a long list of low-level V8 changes. Pure JavaScript apps using Express, Fastify, Hono, NestJS, Prisma, Drizzle, TypeScript, Vite, or webpack usually pass once the dependency tree is current. Apps with native modules, pinned Docker images, old CI actions, or serverless runtime constraints need more care.

Node 22 vs Node 20: what actually improves

Node 22 is not just "Node 20 with a longer support window." It also changes several day-to-day runtime defaults.

Synchronous require() can load many ESM modules

The biggest migration-quality improvement is CommonJS/ESM interoperability. Node 20 CommonJS projects usually had three bad choices when a dependency went ESM-only: convert the app to ESM, use dynamic import() wrappers, or pin an old dependency.

In supported Node 22 releases, CommonJS require() can load synchronous ESM graphs. The important caveat is that modules using top-level await still cannot be loaded synchronously.

// Node.js 20: CommonJS requiring ESM usually fails.
const { execa } = require('execa');

// Older workaround: async boundary just to load a dependency.
async function run() {
  const { execa } = await import('execa');
  return execa('node', ['--version']);
}

// Node.js 22.12+ can require compatible synchronous ESM graphs.
const { execa } = require('execa');

// Still test the exact dependency: top-level await remains async by design.

Do not treat this as permission to remove every ESM workaround blindly. Test the packages you actually load, especially build tools, CLIs, and libraries that initialize asynchronously. If ESM-only dependency churn is your main pain point, also read the ESM-only package migration guide.

Web platform APIs and standard library coverage are better

Node 22 includes a default WebSocket client, stable watch mode, fs.glob() / fs.globSync(), newer V8 JavaScript features such as Array.fromAsync() and Set methods, and continued improvements in web-compatible APIs.

That matters for package health because every removed helper dependency is one fewer package to audit. A Node 22 upgrade is a good time to ask whether you still need:

  • node-fetch or a wrapper when native fetch / Undici primitives are enough;
  • one-off glob dependencies for simple filesystem matching;
  • lodash set/grouping helpers in code that can use native Set methods or grouping APIs;
  • large watcher/process wrappers where node --watch now covers the development case.

For HTTP clients, do not remove a mature package just because Node has fetch. Compare retries, proxy support, streaming ergonomics, and observability in got vs undici vs node-fetch before simplifying production networking code.

Performance improves, but benchmark claims should not drive the migration

Node 22's V8 and runtime updates can improve startup time, short-lived CLI performance, garbage collection behavior, and some server workloads. The Node 22 release announcement specifically calls out V8 updates and Maglev support on supported architectures.

Treat performance as a validation item, not the business case. If you operate a high-throughput API, run your own before/after load test with the same Docker base image, dependency versions, CPU limits, and database latency. If your service is database-bound, the measured improvement may be small even when the runtime is healthier.

The non-negotiable business case is support: Node 20 is EOL. The upside is that Node 22 often also makes package maintenance easier.

Should you upgrade from Node 20 to 22 or jump to Node 24?

Use this decision table before you edit production infrastructure.

SituationBetter targetWhy
You need the smallest safe move off Node 20Node 22It is closer to Node 20 and supported until 2027-04-30.
Your provider supports Node 24 and your tests are strongNode 24It is the newer LTS line and avoids another migration soon.
You have older native addons or closed-source vendor SDKsNode 22 firstFewer simultaneous variables make addon and SDK failures easier to isolate.
You maintain libraries consumed by many Node versionsTest 20, 22, and 24 before raising enginesConsumers may still need broad compatibility; do not raise engines.node casually.
You are modernizing ESM, test runner, Docker, and CI togetherCompare bothThe larger the platform project, the more valuable the Node 22 vs 24 comparison becomes.

A practical compromise is to test both 22 and 24 in CI while production moves to Node 22 first. That lets you leave Node 20 quickly and learn whether Node 24 is ready for the next sprint.

A safe Node 20 to 22 migration plan

1. Inventory every runtime pin

Search for Node 20 in all the places that actually run code:

# Version manager files
cat .nvmrc .node-version 2>/dev/null

# package.json engines and scripts
node -p "require('./package.json').engines"

# Common runtime pins to inspect manually
grep -R "node:20\|node-version: ['\"]20\|NODE_VERSION=20" \
  Dockerfile* .github .gitlab-ci.yml .circleci serverless.yml package.json 2>/dev/null

Then update the real runtime surfaces, not only local development:

# Local developer baseline
nvm install 22
nvm use 22
node --version

# Project pin
echo "22" > .nvmrc

# package.json
{
  "engines": {
    "node": ">=22 <25"
  }
}

# GitHub Actions
- uses: actions/setup-node@v4
  with:
    node-version: '22'

# Dockerfile
FROM node:22-bookworm-slim

Use a specific patch version when reproducibility matters, especially in Docker images and regulated environments. Use a broad LTS tag only if your team accepts automatic runtime movement.

2. Reinstall dependencies under Node 22

Do not trust an existing node_modules directory created under Node 20. Reinstall under Node 22 and rebuild native modules.

rm -rf node_modules
npm ci
npm rebuild
npm test

Use your actual package manager in the real project. PkgPulse readers commonly use npm, pnpm, Bun, or Yarn; the important part is that the install and lockfile check happen under the new runtime.

If the failure is dependency-related, inspect package health before pinning an old version. PkgPulse can help compare package maintenance and ecosystem signals, starting with pnpm vs npm for package-manager decisions and the related Node package guides linked below.

3. Run tests with warnings enabled

Runtime upgrades often surface warnings before they cause crashes. Make warnings visible in CI for at least the migration branch.

NODE_OPTIONS=--trace-warnings npm test
NODE_OPTIONS=--trace-warnings npm run build

Watch for deprecated URL APIs, stale experimental flags, native-addon warnings, ESM interop failures, and packages that declare engines excluding Node 22.

4. Stage the deployment with a rollback plan

A good rollout sequence is:

  1. local install and unit tests on Node 22;
  2. CI matrix with Node 20 and Node 22, then remove Node 20 after confidence is high;
  3. staging deployment on the same image/runtime as production;
  4. smoke tests for login, API routes, background jobs, queues, file uploads, and scheduled tasks;
  5. production canary or low-traffic rollout;
  6. 24-48 hours of error-rate, latency, memory, and job-failure monitoring.

For containers, verify the runtime inside the built artifact rather than assuming the Dockerfile changed:

docker run --rm your-app-image node --version

For serverless, log process.version in a private health endpoint or deployment smoke test. Many failed upgrades happen because CI runs Node 22 while production is still pinned to a managed Node 20 runtime.

What not to change during the runtime upgrade

Keep the Node 20 to 22 migration boring. Avoid bundling unrelated modernization work into the same PR.

Good migration scope:

  • version manager files;
  • engines.node;
  • CI and Docker runtime pins;
  • lockfile/native-addon updates that are required by Node 22;
  • tiny fixes for warnings or actual test failures.

Risky scope creep:

  • converting the whole app from CommonJS to ESM;
  • replacing Jest/Vitest with node:test in the same PR;
  • rewriting HTTP clients around native fetch without feature parity checks;
  • changing package managers;
  • jumping to a new framework release because the runtime changed.

Do those follow-up improvements after production is stable on a supported runtime. For Node version tooling, compare fnm vs nvm vs Volta. For test-runner modernization, compare node:test vs Vitest vs Jest and bun:test vs node:test vs Vitest.

Common failure modes after the upgrade

Native addon install fails

Symptoms: node-gyp errors, missing prebuilt binaries, failures importing sharp, canvas, SQLite packages, bcrypt variants, or older crypto bindings.

Fix path:

  1. confirm the package supports Node 22;
  2. upgrade to a version with Node 22 prebuilds;
  3. rebuild in the production-like environment;
  4. prefer N-API based packages where available.

For addon strategy, see node-gyp vs prebuild vs napi-rs.

ESM loading behavior differs between local and CI

Symptoms: local tests pass, CI fails with ERR_REQUIRE_ESM, ERR_REQUIRE_ASYNC_MODULE, or different module namespace shapes.

Fix path:

  1. verify CI is actually running Node 22.12+ if you rely on improved require(ESM) behavior;
  2. keep dynamic import() for modules with top-level await;
  3. test both the package's default export and named exports;
  4. avoid converting the entire app to ESM in the same upgrade PR unless that was already planned.

The app still runs Node 20 in production

Symptoms: CI says Node 22, production logs say v20.x, or a managed platform silently uses the old runtime.

Fix path:

  • update Docker base images;
  • update buildpack or provider runtime settings;
  • update serverless runtime fields;
  • redeploy from a clean artifact;
  • add a smoke check that records process.version during deployment.

Warnings hide real problems

Symptoms: logs are noisy after upgrade because old url.parse() usage, stale flags, or dependencies emit warnings.

Fix path:

  • capture NODE_OPTIONS=--trace-warnings output in CI;
  • fix warnings in app-owned code first;
  • upgrade or replace stale dependencies only when the warning is real and reachable;
  • do not suppress warnings globally until the migration is understood.

Final recommendation

If you are still on Node.js 20, treat the migration as urgent because upstream support ended on 2026-04-30. Node.js 22 is the safest conservative target when you need a smaller move, and it gives you a supported runtime through 2027-04-30 while improving ESM interop and built-in APIs.

For new projects or teams that can afford one stronger upgrade pass, compare Node 24 before shipping the change. The best 2026 plan is often: leave Node 20 immediately, land on Node 22 if risk is high, and keep a tested path to Node 24 once your dependencies and provider runtime are ready.

Sources checked

  • Node.js previous releases — official status table for Node 20 Iron, Node 22 Jod, and Node 24 Krypton, accessed 2026-05-15.
  • nodejs/Release schedule — upstream start, LTS, maintenance, and end dates for Node major lines, accessed 2026-05-15.
  • Node.js 22 release announcement — Node 22 feature highlights including ESM loading, WebSocket, watch mode, glob, V8, and Maglev notes, accessed 2026-05-15.

Track Node.js runtime package health and download trends at PkgPulse.

Compare pnpm and npm package health on PkgPulse.

Related guides: Node 22 vs Node 24, Node.js 24 migration guide, ESM-only package migration, and Best JavaScript runtimes in 2026.

See the live comparison

View pnpm vs. npm on PkgPulse →

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.