Node.js Release History and LTS Strategy — What Every Developer Should Know in 2026
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 type | Promotion | LTS period | Who should use it |
|---|---|---|---|
| Even (18, 20, 22) | Promoted to LTS in October | 30 months total | Production workloads |
| Odd (17, 19, 21) | Never promoted to LTS | 6 months only | Early adopters, feature testing |
| Current | Receives new features every ~2 weeks | N/A | Non-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.jsThe 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
| Scenario | Recommended version |
|---|---|
| New production service | Node.js 22 LTS |
| Existing service on 18 LTS | Upgrade to 20 or 22 LTS |
| Testing new JS features | Node.js 23 (current) |
| Edge/serverless runtime | Node.js 22 (widest support) |
| Long-term stability priority | Node.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.inspectoutput 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
.nvmrcand 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-compatagainst your dependencies to identify packages that do not support your target Node.js version. - Use
enginesinpackage.jsonto document the minimum supported version and failnpm installif 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, andstructuredClone— 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, andpackage.jsonenginesfield to ensure consistent behavior across development, CI, and production. - Run
npm rebuildafter upgrading Node.js versions if your project uses native addons — the binary ABI changes between major versions.
Advertisement