Back to Blog

Node.js 26.9 Turns FFI On by Default; Bench, Web Workers and VFS Stay Flagged

Node.js 26.9.0 turns node:ffi on by default while node:bench, Web Workers and VFS module loading stay behind flags, which matters for code review, version pinning and production.

Umut Korkmaz

Published: 4 min read

Node.js 26.9.0 landed yesterday as a Current release, and the release notes read like a list of npm packages Node has decided to absorb: a benchmark runner, a foreign function interface, browser-style Web Workers and a virtual file system that now plugs into the module loaders. Read next to the 26.9.0 API docs, though, only one of those is switched on by default, and it is the one with the sharpest edges: node:ffi.

What shipped

  • node:ffi is enabled by default. The module arrived in v26.1.0 and is still marked Stability 1, Experimental.
  • node:bench is a new module and CLI benchmark runner with reporters and an explicit createRunner API. The docs mark it 1.0, Early Development, and a commit in this same release put it behind --experimental-bench.
  • Web Workers: a mostly browser-compatible global Worker, built on node:worker_threads, with Web Platform Tests added. It needs --experimental-web-worker.
  • node:vfs, behind --experimental-vfs since v26.4.0, gains mount(), so virtual files become reachable through node:fs, require() and import.
  • Smaller items: a generic MAC API, ciphers and hashes discovered from OpenSSL providers, an experimental DTLS API, Histogram meanCI plus CBOR export/import in perf_hooks, and embedders can supply a builtin code cache without a snapshot.

FFI on by default changes what you review

The FFI docs are blunt that the API is unsafe. Invalid pointers, wrong symbol signatures or touching freed memory "can crash the process or corrupt memory", and the module does not track pointer validity, ownership or native object lifetimes.

The part I care about as a reviewer is visibility. Calling native code from a dependency used to leave traces: a compiled addon, a build step, prebuilt binaries in the tarball. With node:ffi available by default in builds that include it, a plain .js file can dlopen a shared library and call its symbols. If that library is already on the machine, nothing about the install has to look different.

There are two documented controls:

# Turn the module off where you don't need it
node --no-experimental-ffi server.js

# Under the Permission Model, FFI throws ERR_ACCESS_DENIED unless allowed
node --permission --allow-ffi app.js

node:bench is a runner, not a verdict

The API is small: suite(), bench(), and a context where you call start() and end(operations) around the measured work. From the CLI, node --experimental-bench --bench 'benchmarks/**/*.js' runs files serially, by default each in its own child process, and a JSON reporter is available.

What I like is what it leaves out. The bench docs say it does not designate a baseline or produce a pass/fail comparison. It exposes raw samples, stable benchmark identities, parameters and tags, and leaves comparison policy to higher-level tools. That is the same split I use in Quorate: collect raw signal in one layer, apply deterministic gates in another. The "Measurement integrity" section is worth reading on its own, including the warning that a statistically consistent result does not prove the benchmark measured the work you intended.

For a team, I would not rip out an existing benchmark library for an early-development, flag-gated module. I would write new micro-benchmarks in a shape that ports easily later.

Web Workers and VFS: useful, still opt-in

The Web Worker global matters for code meant to run in both browsers and Node, which is common when React or Next.js front ends sit next to Node services. The limits are documented: scripts load only from file:, data: and blob: URLs, there is no SharedWorker, and the docs still point you to worker_threads when you need workerData, resource limits or a custom env.

The VFS docs pitch the module at tests, fixtures and embedded assets, and a companion commit mounts single executable application assets as a VFS. Mount points live in a reserved namespace, so virtual paths never shadow real ones. The VFS docs also say plainly that it is not a sandbox, permission system or access-control mechanism. Don't use it to isolate anything.

Why this belongs on Current

Every headline API here is marked experimental in the 26.9.0 docs, and node:bench sits at the earliest stage. That is what the Current line is for: ship, collect feedback, change shape. Even the smaller commits carry behavior changes. For example, ** globs in fs no longer descend into symlinks unless you are following symlinks. Production should stay on an LTS line and treat 26.x as the place to experiment.

What I'm doing about it

  • Pinning exact Node versions in Dockerfiles and systemd units instead of floating on a major tag, so a minor release can't quietly switch on a capability.
  • Adding --no-experimental-ffi to any 26.x service that has no reason to make native calls.
  • Adding node:ffi to the patterns I look for when reviewing dependency diffs and AI-generated patches, next to child_process and dynamic require().
  • Trying node:bench on a branch, feeding its JSON into a small script with our own thresholds, before trusting any number it prints.

Sources

Primary references used for this post.

  1. nodejs.org/en/blog/release/v26.9.0 (opens in a new tab)
  2. nodejs.org/docs/v26.9.0/api/ffi.html (opens in a new tab)
  3. nodejs.org/docs/v26.9.0/api/bench.html (opens in a new tab)
  4. nodejs.org/docs/v26.9.0/api/vfs.html (opens in a new tab)
  5. nodejs.org/docs/v26.9.0/api/globals.html (opens in a new tab)
  6. nodejs.org/docs/v26.9.0/api/cli.html (opens in a new tab)

About the author

Umut Korkmaz

Lead Software Engineer

I'm a senior software engineer in Istanbul. Here I write about the tools and releases I've shipped to production myself, and what they taught me along the way.

Picked by shared topics.