Blog / 02

Node.js 26.9.0 Adds node:bench, Web Workers, FFI

WebRestart

On September 16, 2026 the Node.js project shipped v26.9.0 on the Current line. Every notable change in it is SEMVER-MINOR, so nothing here breaks — but three of the additions change what you can do without a dependency. Node now has a built-in benchmarking module (node:bench), a browser-compatible Worker global, and a foreign function interface that is enabled by default rather than hidden behind an opt-in flag.

The timing matters more than usual. Per the Node.js release schedule, the 26.x line enters Active LTS on October 28, 2026, and 24.x drops into maintenance on October 20. Whatever lands on Current between now and then is what your LTS baseline will look like for the next two and a half years. This is the release to read properly.

node:bench

node:bench (#65606) is a benchmarking framework in core, shaped like node:test. It is new in 26.9.0, carries a stability of 1.0 — Early Development, and requires --experimental-bench.

import { bench, suite } from 'node:bench';

suite('URL', () => {
  const input = 'https://example.com/a?b=c';

  bench('construct', { samples: 30 }, (b) => {
    const operations = 10_000;
    b.start();
    for (let i = 0; i < operations; i++) new URL(input);
    b.end(operations);
  });
});
node --experimental-bench --bench benchmark.mjs

The API is bench() and suite()/describe(), with before, after, beforeEach and afterEach hooks, plus bench.skip() and bench.only(). The BenchContext handed to each run gives you start()/end(operations) for manual timing, record() for samples you measured yourself, and diagnostic() for annotations. run() returns a BenchmarksStream of lifecycle events; spec and json reporters ship in node:bench/reporters.

Two details make this more than a convenience. Results come with statistics — mean, median, standard deviation and confidence intervals — rather than a single number you have to interpret. And --bench-isolation=process runs each file in a fresh child process, which is the difference between measuring your code and measuring whatever the previous benchmark left in the JIT. The matching perf_hooks work landed alongside it: a Histogram meanCI API in the same PR, and CBOR export/import (#65434) so histograms can be shipped between processes.

Web Workers

There is now a global Worker class (#64894), documented as "a mostly browser-compatible implementation of Web Workers of the HTML Standard, implemented on top of node:worker_threads". Stability is 1 — Experimental and it needs --experimental-web-worker.

// worker.js
addEventListener('message', (event) => {
  postMessage(`${event.data} from ${name}!`);
});

// main.js
const worker = new Worker('./worker.js', { name: 'greeter' });
worker.addEventListener('message', (event) => {
  console.log(event.data);
  worker.terminate();
});
worker.postMessage('Hello');

The deviations from the spec are the interesting part, and they are all consequences of Node not being a browser. Scripts load from the local filesystem, so only file:, data: and blob: URLs are accepted — no network fetch. Node has no origin model, so location.origin is always 'null'. close() terminates the worker immediately instead of following the specification's shutdown algorithm. SharedWorker is not implemented at all, since it is defined in terms of origins.

If you maintain a library that runs in both the browser and Node, this is the one that saves you an abstraction layer.

FFI on by default

node:ffi arrived in v26.1.0 as a flagged experiment. In 26.9.0 the module is enabled by default (#65475); you disable it with --no-experimental-ffi. Stability is still 1 — Experimental.

It loads dynamic libraries and calls native symbols directly — dlopen() and a DynamicLibrary class, function signatures declared as { arguments: ['int32', 'int32'], return: 'int32' }, callbacks via registerCallback(), and helpers for reading and writing through native pointers. The practical effect is that a class of native-addon dependency becomes optional.

It is also unsafe by construction: a wrong signature or a stale pointer corrupts memory or kills the process. Under the Permission Model it stays restricted and requires --allow-ffi — which is a good argument for the audit workflow we covered in Node.js 24.20.0, where you collect what your app actually touches before you start granting anything.

Also in 26.9.0

  • crypto — a generic MAC API (#65553), and cipher and hash discovery from OpenSSL providers (#65484).
  • DTLS — an experimental DTLS API (#63182). Relevant if you touch WebRTC or media over UDP.
  • vfs — the virtual filesystem now integrates with the CJS and ESM module loaders (#63653).
  • src — embedders can supply a builtin code cache without a snapshot (#65352).

On the LTS line, 24.21.0 shipped earlier this month with OpenSSL 3.5.8, Undici 7.29.1, root certificates updated to NSS 3.126, a non-throwing MIMEType.parse, and faster net.BlockList. Neither release is a security release.

What to do

Nothing in 26.9.0 is urgent, and none of it is production-ready — all three headline features are experimental. But 26.x becomes your LTS in five weeks, so this is the moment to find out whether node:bench can replace whatever benchmarking dependency you are carrying, and whether node:ffi removes a native addon from your build.

docker run --rm node:26-alpine node --version

Full detail is in the 26.9.0 release notes, the node:bench docs and the node:ffi docs.


Planning a jump to Node 26 when it goes LTS in October, and want to know what will break before you find out in production? Talk to us — runtime audits and upgrade paths, without the three-month project.