Node.js 21 was released on October 17th, continuing the project’s steady six-month cadence for odd-numbered (current) releases. While odd-numbered releases don’t become LTS (that honour goes to the even-numbered ones — Node.js 20 entered LTS this same week), they serve as the proving ground for features that will eventually land in the next LTS line.
So what does Node.js 21 bring to the table, and why should you care even if you’re firmly planted on an LTS release?
Built-in WebSocket Client#
The headline feature is the stable, built-in WebSocket client. Node.js has had experimental WebSocket support for a while, but 21 marks a significant step forward in making it a proper first-class citizen.
For years, the Node.js ecosystem has relied on packages like ws, socket.io, and others for WebSocket functionality. These libraries are excellent and aren’t going anywhere, but having a built-in WebSocket global that aligns with the browser API is a meaningful step toward runtime compatibility.
const ws = new WebSocket('wss://example.com/socket');
ws.addEventListener('open', () => {
ws.send('Hello from Node.js 21');
});
ws.addEventListener('message', (event) => {
console.log('Received:', event.data);
});If you write code that needs to run in both browser and server contexts, this is a genuine quality-of-life improvement. The same WebSocket code now works without conditional imports or polyfills. It’s part of a broader trend in Node.js toward implementing Web Platform APIs — fetch, WebCrypto, structuredClone, and now WebSocket.
I’ve been advocating for this kind of convergence for years. The artificial barrier between “browser JavaScript” and “server JavaScript” has been a source of unnecessary complexity, and watching it gradually dissolve is satisfying.
V8 11.8 and What It Brings#
Node.js 21 ships with V8 11.8, which brings several improvements from the JavaScript engine side. The ArrayBuffer resizing and SharedArrayBuffer growth features are now available, which is particularly relevant for performance-sensitive applications that work with binary data.
Array grouping with Object.groupBy() and Map.groupBy() is also now available. This has been a long time coming — it’s one of those utility functions that every project implements differently:
const inventory = [
{ name: 'asparagus', type: 'vegetable' },
{ name: 'banana', type: 'fruit' },
{ name: 'carrot', type: 'vegetable' },
];
const grouped = Object.groupBy(inventory, (item) => item.type);
// { vegetable: [...], fruit: [...] }
Lodash’s groupBy has been one of the most imported functions in the Node.js ecosystem for a decade. Having a native equivalent reduces bundle size and dependency count for a lot of projects.
The --experimental-default-type Flag#
Node.js 21 stabilises the --experimental-default-type flag, allowing you to set the default module system for .js files to ESM. This continues the long, sometimes painful migration from CommonJS to ES modules.
The ESM transition in Node.js has been one of the more contentious journeys in the JavaScript ecosystem. It’s been years of dual-format packages, conditional exports, .mjs extensions, and "type": "module" in package.json. Every release chips away at the friction, but we’re still not at the point where ESM “just works” in all scenarios.
This flag helps by letting you run an entire project as ESM without modifying package.json or renaming files. It’s particularly useful for quick scripts and prototyping where the ceremony of ESM configuration feels heavy.
Test Runner Improvements#
The built-in test runner (node:test) continues to mature. In Node.js 21, glob pattern support for specifying test files is now stable, and there are improvements to the assertion library.
I’ve been watching the built-in test runner with interest since it was introduced. Jest and Vitest dominate the testing landscape, but having a zero-dependency test runner built into the runtime has clear advantages for certain use cases — particularly CI/CD pipelines where minimising install time matters, or for library authors who want to avoid testing framework dependencies.
import { describe, it } from 'node:test';
import assert from 'node:assert';
describe('Array', () => {
it('should return -1 when value is not present', () => {
assert.strictEqual([1, 2, 3].indexOf(4), -1);
});
});The built-in runner isn’t going to replace Jest for complex applications with snapshot testing and mocking needs, but for straightforward test suites, it’s increasingly capable.
The LTS Transition Context#
What makes this release cycle interesting is the timing. Node.js 20 simultaneously entered Active LTS status, which means it’s now the recommended version for production deployments. Node.js 21 is “Current” — it gets the latest features but only has a short support window.
For production systems, stick with Node.js 20 LTS. But if you’re starting new projects or maintaining libraries, testing against Node.js 21 now means fewer surprises when Node.js 22 arrives next year and eventually becomes LTS.
My advice for teams: set up your CI matrix to test against both the current LTS and the latest Current release. It’s minimal effort and catches compatibility issues months before they become urgent.
My Take#
Node.js 21 isn’t a revolutionary release, but it’s a solid evolutionary one. The built-in WebSocket client, continued Web Platform API alignment, and V8 improvements all point in the right direction.
What I find most encouraging is the project’s commitment to reducing the gap between server-side and client-side JavaScript. Every time Node.js implements another Web Platform API natively, it reduces the need for polyfills, simplifies isomorphic code, and makes the JavaScript ecosystem more coherent.
The runtime landscape is more competitive than ever — Deno and Bun are pushing the envelope in different ways — and that competition is clearly driving Node.js to iterate faster and adopt standards more aggressively. That’s healthy for everyone who writes JavaScript.
If you’re on Node.js 18 LTS, plan your upgrade to 20 LTS. If you’re already on 20, try 21 in your development environment and CI. And keep building — the JavaScript runtime ecosystem has never been more capable.
