Node.js Release History and LTS Strategy — What Every Developer Should Know in 2026

Sanjeev SharmaSanjeev Sharma
7 min read

Advertisement

Introduction

Why This Matters

Choosing the wrong Node.js version for a production deployment is one of the most common — and most avoidable — infrastructure mistakes. Running an End-of-Life version means no security patches. Running an odd-numbered current release means no LTS promotion. Running a version more than one major behind means missing V8 engine improvements, new Web-compatible APIs, and built-in ESM support that eliminates entire categories of dependencies.

This guide explains the release model, what changed in the milestone versions from Node.js 17 onward, and how to pick the right version for your use case in 2026.

The Node.js Release Model

Node.js follows a predictable release cadence:

Release typePromotionLTS periodWho should use it
Even (18, 20, 22)Promoted to LTS in October30 months totalProduction workloads
Odd (17, 19, 21)Never promoted to LTS6 months onlyEarly adopters, feature testing
CurrentReceives new features every ~2 weeksN/ANon-production only

The practical rule: use an even-numbered LTS version for any production deployment. Odd-numbered releases are staging grounds for features that stabilize in the next even release.

Key Changes by Version

Node.js 17 — OpenSSL 3.0 and Readline Promises

Node.js 17 shipped OpenSSL 3.0 (replacing 1.1.1) and introduced the promise-based Readline API:

import * as readline from 'node:readline/promises'
import { stdin as input, stdout as output } from 'process'
 
const rl = readline.createInterface({ input, output })
const answer = await rl.question('What is your Node.js version? ')
console.log(`You said: ${answer}`)
rl.close()

OpenSSL 3.0 tightened restrictions on algorithms and key sizes. Apps using legacy algorithms may throw ERR_OSSL_EVP_UNSUPPORTED — use --openssl-legacy-provider as a temporary workaround while updating the offending dependency.

Node.js 18 — Stable Fetch, Test Runner, and Web Streams

Node.js 18 (LTS until April 2025) was the first release to ship the native fetch API without a flag — a major milestone for ecosystem convergence:

// No node-fetch or axios needed for basic HTTP requests
const res = await fetch('https://api.example.com/users')
const users = await res.json()
 
// Native test runner — no Mocha or Jest needed for simple tests
import { test, describe } from 'node:test'
import assert from 'node:assert'
 
describe('addition', () => {
  test('1 + 1 equals 2', () => {
    assert.strictEqual(1 + 1, 2)
  })
})

Node.js 18 also shipped the Web Streams API (ReadableStream, WritableStream, TransformStream) and structuredClone() — both Web Platform APIs that improve runtime compatibility with browsers and edge environments.

Node.js 20 — Stable Test Runner and Permission Model

Node.js 20 (LTS until April 2026) stabilized the built-in test runner and introduced an experimental permission model:

// Stable test runner with mocking, coverage, and watch mode
import { test, mock } from 'node:test'
import assert from 'node:assert'
 
test('mock a module function', () => {
  const fn = mock.fn(() => 42)
  assert.strictEqual(fn(), 42)
  assert.strictEqual(fn.mock.calls.length, 1)
})
# Permission model — restrict what the process can access
node --experimental-permission \
     --allow-fs-read=/app \
     --allow-net=api.example.com \
     server.js

The permission model is the Node.js equivalent of Deno's security model — deny by default, allow explicitly.

Node.js 22 — Native require() for ES Modules

Node.js 22 (LTS) shipped the most requested interoperability feature: require() for ES modules. This eliminates the "cannot use require() with ESM" error that plagued the ecosystem for years:

// CommonJS file can now require() an ESM module
const { myFunction } = require('./esm-module.mjs')
 
// Top-level await is also supported in --input-type=module mode
const data = await fetch('https://api.example.com/config').then(r => r.json())

Node.js 22 also ships V8 12.4, bringing Array.fromAsync, Promise.withResolvers, and iterator helper methods:

// Promise.withResolvers — create a deferred promise
const { promise, resolve, reject } = Promise.withResolvers()
setTimeout(() => resolve('done'), 1000)
const result = await promise  // 'done'
 
// Array.fromAsync — collect an async iterable into an array
const lines = await Array.fromAsync(readLines('/etc/hosts'))

Choosing the Right Version in 2026

ScenarioRecommended version
New production serviceNode.js 22 LTS
Existing service on 18 LTSUpgrade to 20 or 22 LTS
Testing new JS featuresNode.js 23 (current)
Edge/serverless runtimeNode.js 22 (widest support)
Long-term stability priorityNode.js 20 LTS (until April 2026)

In 2026, Node.js 18 is End-of-Life. If your service is still on 18, upgrading to 22 LTS is urgent — you are not receiving security patches.

Upgrading Node.js Versions Safely

# Use nvm to manage multiple Node.js versions
nvm install 22
nvm use 22
 
# Check your app runs on the new version
npm test
 
# Check for deprecated API usage
node --pending-deprecation server.js
 
# Update engine field in package.json
# "engines": { "node": ">=22.0.0" }

Common breaking changes to check when upgrading major versions:

  • OpenSSL algorithm restrictions (17+)
  • ESM and CJS interoperability changes (22)
  • V8 engine garbage collection behavioral changes (affects setTimeout timing tests)
  • util.inspect output format changes (affects snapshot tests)

Common Mistakes

Using an odd-numbered release in production. Odd releases (17, 19, 21, 23) are not promoted to LTS and receive only 6 months of support. They should never run in production.

Staying on an EOL version. EOL versions receive no security patches. Any discovered vulnerability in Node.js core, OpenSSL, or V8 remains unpatched permanently.

Upgrading Node.js without checking native addon compatibility. Packages like better-sqlite3, sharp, and canvas compile native code against the Node.js ABI. Always run npm rebuild after upgrading and check for pre-built binary availability.

Best Practices

  • Pin your Node.js version in .nvmrc and your CI Dockerfile so all environments use identical versions.
  • Subscribe to the Node.js security mailing list or watch the GitHub releases page for security advisories.
  • Test on the next LTS release 3–6 months before your current version reaches EOL — this gives time to fix compatibility issues before the deadline.
  • Run npx node-check-compat against your dependencies to identify packages that do not support your target Node.js version.
  • Use engines in package.json to document the minimum supported version and fail npm install if the constraint is not met.

Key Takeaways

  • Even-numbered Node.js releases (18, 20, 22) are promoted to LTS and receive 30 months of support — these are the only versions appropriate for production.
  • Odd-numbered releases (17, 19, 21) are feature previews that reach End-of-Life after 6 months and should never run in production.
  • Node.js 18 shipped native fetch, Web Streams, and structuredClone — eliminating several common utility dependencies.
  • Node.js 20 stabilized the built-in test runner and introduced the experimental permission model for process-level sandboxing.
  • Node.js 22 LTS landed native require() for ES modules, resolving years of CJS/ESM interoperability friction.
  • As of 2026, Node.js 18 is End-of-Life — any service still running 18 is not receiving security patches and should upgrade to 22 LTS.
  • Pin your Node.js version in .nvmrc, Dockerfile, and package.json engines field to ensure consistent behavior across development, CI, and production.
  • Run npm rebuild after upgrading Node.js versions if your project uses native addons — the binary ABI changes between major versions.

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro

Related reading