Node.js 26.9: Web Workers, VFS, FFI, and node:bench

Published on 9/18/2026By Prakhar Bhatia
Node.js 26.9: Web Workers, VFS, FFI, and node:bench

Node.js 26.9.0 arrived on September 16, 2026 with an unusually dense set of runtime APIs: experimental Web Workers, module loading from a mounted virtual file system, an FFI module enabled by default, a built-in benchmark runner, crypto provider discovery, histogram exchange, and experimental DTLS.

This article covers those 26.9 changes only. It does not repeat the broader decision between Node.js 24 LTS and Node.js 26 Current. If that is the decision you are making, start with our Node.js 24 LTS vs 26 Current production upgrade guide.

The useful question here is narrower: which 26.9 features solve a real problem today, which are still experiments, and what should a team test before building around them?

Node.js 26.9 in one table

The official 26.9.0 release notes list the notable semver-minor changes.

AreaWhat changed in 26.9Status or caution
Web platformWeb Worker API supportExperimental; requires --experimental-web-worker
Virtual file systemMounted VFS paths work with CJS and ESM loadersExperimental VFS surface
Single executablesAssets can be exposed as a read-only VFSEarly development; incompatible with some SEA modes
FFInode:ffi enabled by defaultExperimental; native memory and ABI risks remain
BenchmarkingNew node:bench moduleEarly development; requires --experimental-bench
Performance dataHistogram confidence interval and CBOR exchangeValidate exchange and statistical assumptions
CryptoGeneric MAC API and OpenSSL provider discoveryTest provider and compliance configurations
NetworkingExperimental DTLS APIEarly API; security-sensitive protocol work
EmbeddingBuilt-in code cache without a snapshotMainly relevant to runtime embedders

Several headline features are experimental. "Included in Node" does not mean "stable enough to expose as an application contract." Pin 26.9 during evaluation and follow later release notes for API changes.

Experimental Web Workers arrive in Node

Node has long supported parallel JavaScript through node:worker_threads. Version 26.9 adds experimental support for the browser-style Web Worker API behind this flag:

node --experimental-web-worker app.mjs

The significance is compatibility. Libraries written around Worker, postMessage, MessageEvent, and browser worker conventions can move closer to a shared implementation across browsers and Node. That is useful for test infrastructure, isomorphic packages, and code that already targets web APIs.

Do not assume the new API makes a Node process behave like a browser. Node's documentation notes that each worker has its own V8 isolate, JavaScript heap, and event loop. Workers still share process-wide resources, including libuv's thread pool and some native or addon state. A worker is not the same isolation boundary as a child process or container.

The practical comparison is:

NeedStart with
Reuse a browser-oriented worker libraryExperimental Web Worker API
Use mature Node-specific worker controlsnode:worker_threads
Isolate crashes, native state, or privileges more stronglyChild process or external sandbox
Parallelize CPU work with a controlled poolworker_threads plus a pool design

A minimal experiment may look familiar to browser developers:

// main.mjs
const worker = new Worker(new URL("./worker.mjs", import.meta.url), {
  type: "module",
})

worker.addEventListener("message", (event) => {
  console.log(event.data)
})

worker.postMessage({ values: [3, 5, 8] })
// worker.mjs
globalThis.addEventListener("message", (event) => {
  const total = event.data.values.reduce((sum, value) => sum + value, 0)
  globalThis.postMessage({ total })
})

Run compatibility tests against the exact API behavior your library needs. Check module loading, error propagation, structured cloning, transferable objects, termination, and shutdown. A library that detects globalThis.Worker may choose a new code path under 26.9, so adding the flag can change behavior even before your application directly creates a worker.

VFS becomes part of normal module loading

Node introduced the node:vfs surface before 26.9. The new release connects mounted virtual file systems to the APIs applications already use. According to the Node.js VFS documentation, files under a mounted VFS can be reached through node:fs, CommonJS require(), and ESM import().

import fs from "node:fs"
import vfs from "node:vfs"

const files = vfs.create()
files.mkdirSync("/lib")
files.writeFileSync(
  "/lib/message.mjs",
  "export const message = 'loaded from memory'",
)

const mountPoint = files.mount()

console.log(fs.readFileSync(`${mountPoint}/lib/message.mjs`, "utf8"))

const module = await import(`${files.mountPointURL}/lib/message.mjs`)
console.log(module.message)

files.unmount()

The mount point lives in a reserved namespace and is selected by Node. Do not construct a path based on an observed mount point. Use the value returned by mount() or the mountPoint and mountPointURL properties.

Module resolution inside the mount follows the ordinary CJS and ESM rules, but the mounted namespace and real file system do not fall through to each other. If a path belongs to the VFS and a requested file is missing, resolution fails instead of checking an equivalent disk path. That prevents a virtual path from ambiguously shadowing a real directory.

Node 26.9 also defines unmount behavior. Modules loaded from that mount are invalidated when it is unmounted, so a later mount can load new contents instead of returning the old module from cache. Code that already started keeps running, and objects already created from a module do not disappear. Applications must avoid unmounting while a module is still loading or while code expects its resources to remain present.

ZIP files become VFS providers

The new ZipProvider exposes archive entries through the VFS API. That supports applications which ship a package of modules, templates, rules, or other assets and want them available through familiar file and module interfaces.

This reduces custom extraction and lookup code, but it changes the trust boundary. An archive from an untrusted source still needs limits and validation. Check compressed and expanded sizes, entry count, path handling, symlinks where supported, and the behavior of native addons. Loading JavaScript from an archive executes JavaScript; VFS is packaging infrastructure, not a sandbox.

Node's VFS docs say native .node addons can be required from a mounted VFS. Because the operating-system loader cannot open a virtual path, Node loads the bytes through a private temporary image. Test cleanup, endpoint security interactions, code-signing expectations, and platform support before relying on that path.

Single executable applications can use a mounted VFS

Node 26.9 lets a Single Executable Application expose bundled assets through a read-only virtual file system. Set "useVfs": true in the SEA configuration. The injected main script runs from the VFS root, so relative require() calls and __dirname-relative asset reads work more like a conventional project tree.

{
  "main": "dist/main.cjs",
  "output": "dist/app.blob",
  "useVfs": true,
  "assets": {
    "templates/index.html": "assets/index.html",
    "config/default.json": "config/default.json"
  }
}

The SEA documentation marks VFS assets as early development. It also states that useVfs cannot be combined with useSnapshot or useCodeCache. The configuration parser rejects those combinations.

Test assumptions that normally depend on the disk layout:

  • __filename and __dirname point inside the virtual file system;
  • packages that walk upward looking for configuration may stop at the mount boundary;
  • writable data must go to a real writable location;
  • tools that expect source files beside the executable may need explicit paths;
  • native addons take the temporary-image path described by the VFS docs.

VFS-backed SEA assets are useful for command-line tools and controlled deployment artifacts. The experimental status still argues for a small proof of concept before migrating a packaging pipeline.

FFI is enabled by default, but remains experimental

Node 26.9 enables the FFI module by default. Users no longer need the earlier enablement path simply to import it. The module remains experimental.

Foreign Function Interface support lets JavaScript call native functions without writing the usual Node-API addon wrapper for every operation. That can make small integrations with existing C libraries faster to prototype. It also removes safety that a carefully written binding normally provides.

The risks are concrete:

  • an incorrect signature can corrupt memory;
  • pointer lifetime can outlive the object that owns the memory;
  • the process can crash outside JavaScript exception handling;
  • callbacks can cross threads or execute after cleanup;
  • structure layout, alignment, calling convention, and symbol names vary by platform;
  • a library upgrade can change an ABI without TypeScript noticing.

Treat FFI definitions as native code. Pin the target library, test on every operating system and architecture you ship, fuzz boundary inputs, and keep the wrapper narrow. Do not pass untrusted pointer values or let application code choose arbitrary libraries and symbols.

For a production service, the decision is not "FFI or no FFI." Compare FFI with a Node-API addon, a subprocess boundary, WebAssembly, or a separate service. FFI is attractive for low-overhead calls into a stable local library. A subprocess is easier to contain when the library is crash-prone or handles hostile input.

node:bench gives Node a benchmark runner

Node 26.9 adds node:bench, available only with the --experimental-bench flag. The benchmark runner documentation marks it Stability 1.0, Early Development.

import { bench, suite } from "node:bench"

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

  bench("construct URL", { samples: 30 }, (b) => {
    const operations = 10_000
    let total = 0

    b.start()
    for (let i = 0; i < operations; i += 1) {
      total += new URL(input).pathname.length
    }
    b.end(operations)

    if (total === 0) throw new Error("prevent dead-code assumptions")
  })
})
node --experimental-bench benchmark.mjs

The runner can define benchmarks in the current process and run a benchmark file in a fresh child process. That distinction matters because warm process state, garbage collection, JIT compilation, module caches, and background work can distort a microbenchmark.

Built-in tooling does not make a benchmark representative. Decide what the result needs to predict. If you care about request throughput, test the service with realistic concurrency and payloads. If you care about one parser, isolate inputs, warmup, allocations, correctness checks, and variance. Record CPU model, operating system, Node version, power mode, flags, and dependency versions.

Because the API is early development, keep benchmark files internal and pin Node in CI. Avoid building a public package interface around node:bench until its stability changes.

Histograms gain exchange and confidence information

The same release adds a meanCI API to performance histograms and CBOR import/export for histogram exchange. These features can help benchmark tooling transfer recorded distributions between processes and report uncertainty around a mean.

A confidence interval is not a cure for a biased benchmark. Samples taken under unrealistic conditions can be measured precisely and still predict nothing useful. Check independence, warmup, outliers, and whether the mean is the right statistic. Latency work often needs percentiles and the whole distribution more than one average.

CBOR exchange should preserve the histogram's intended semantics across producers and consumers. Version the envelope around stored results, include the Node version and benchmark metadata, and reject incompatible inputs explicitly.

Crypto learns more from OpenSSL providers

Node 26.9 adds a generic MAC API and allows cipher and hash discovery from OpenSSL providers. This matters to applications that load provider-specific algorithms, including regulated or specialized deployments where the available cryptography is not limited to Node's built-in assumptions.

Provider discovery increases flexibility and the amount of configuration that needs testing. Enumerate the algorithms available in the actual production image. Verify startup behavior when a provider is missing or misconfigured. Test compliance modes and error paths instead of assuming an algorithm advertised in development exists in a minimal container.

The release also expands FIPS-related options, including stricter handling for operations reported through OpenSSL's indicator callback. Compliance teams should read the Node and OpenSSL limitations carefully. A flag does not prove that native addons, alternate library contexts, or another copy of libcrypto follow the same policy.

The generic MAC API is useful when an application needs algorithms exposed through providers rather than only familiar high-level helpers. Keep algorithm selection on an allowlist. Do not accept arbitrary algorithm names, keys, or provider choices from an external request.

DTLS enters as an experimental API

Datagram Transport Layer Security applies TLS-like security to datagram transport. Node 26.9 introduces an experimental DTLS API.

Potential use cases include real-time media, telemetry, constrained systems, and protocols where UDP semantics matter. DTLS is security-sensitive and operationally different from ordinary TLS over TCP. Applications must account for packet loss, reordering, replay protection, MTU, retransmission, certificate handling, and denial-of-service pressure during handshakes.

Do not replace a mature DTLS stack solely to remove a dependency. Start with interoperability tests against every peer implementation, adverse network simulation, certificate rotation, session resumption, and packet captures that confirm the protocol behavior. The API may change while experimental.

What should teams adopt first?

The safest immediate use of 26.9 is evaluation.

node:bench is easy to try in an internal benchmark repository because it does not need to enter the production request path. Web Workers make sense for a library that already has a browser-worker implementation and can tolerate an experimental flag. VFS and SEA integration are worth testing for packaged command-line tools. FFI belongs in a narrowly scoped native integration with strong platform tests. DTLS needs the most protocol and security review.

Use a feature gate and pin the runtime:

{
  "engines": {
    "node": "26.9.x"
  }
}

Run the existing application suite without experimental flags first. Then add one flag or feature at a time. Record startup warnings, memory, shutdown, module loading, packaging, and rollback behavior. Keep production on the established path until the new feature has a specific owner and measurable benefit.

Create a small acceptance record for each experiment. Include the Node build, required flag, owner, workload, expected benefit, test results, known limitations, and removal plan. Experimental APIs tend to spread through helper libraries after a successful demo. A narrow adapter keeps that dependency visible and makes a later API change cheaper.

Feature: node:bench
Runtime: Node.js 26.9.x
Flag: --experimental-bench
Scope: internal performance repository only
Benefit: one runner for isolated JavaScript microbenchmarks
Exit condition: remove or migrate if the API changes before stabilization
Owner: runtime performance team

For Web Workers, VFS, FFI, and DTLS, add a test that fails when the experimental flag or API disappears. This produces a clear upgrade failure instead of a production path that changes silently. Keep the experiment out of shared foundational packages unless every consumer is prepared to pin the same runtime line.

Review each feature at the boundary it changes

The new APIs touch different failure boundaries, so one generic upgrade checklist is too shallow.

For Web Workers, inspect libraries that branch on the presence of the global Worker. Test message cloning, error events, termination, process shutdown, memory ceilings, and any native addon used from more than one isolate. Measure worker startup and pooling rather than assuming browser-compatible syntax changes performance.

For VFS, test every path that loads modules or assets. Include CJS, ESM, dynamic imports, require.resolve(), package scopes, missing files, unmount and remount, ZIP archives, and native addons. Verify that code never constructs a mount path itself. Confirm that writes go to an intended real location instead of failing inside a read-only SEA mount.

For FFI, review the signature and memory ownership for every symbol. Run the wrapper under sanitizers where possible, exercise invalid and boundary inputs, and test all supported architectures. Confirm what happens when the shared library is absent, the symbol has moved, or the ABI version is wrong. A JavaScript try/catch cannot recover from every native crash.

For node:bench, keep correctness assertions beside the measurement. Compare a result across repeated fresh processes, record the environment, and inspect variance. Do not merge a performance change because one short microbenchmark improved once. Run the application-level load or latency test that connects the microbenchmark to user-visible work.

For crypto provider discovery, start the exact container image with the intended OpenSSL configuration. List the algorithms, exercise approved and rejected operations, and capture failure output. Compliance claims need review beyond a successful call on a developer machine.

For DTLS, test against real peers under loss, duplication, reordering, and constrained MTU. Include certificate expiry, key rotation, handshake floods, idle sessions, and clean shutdown. Protocol interoperability and resource exhaustion matter as much as the happy-path exchange.

A feature should leave evaluation with an owner, evidence, and a decision. "Experimental" can mean continue testing, isolate behind a flag, or decline adoption. It should not mean the dependency is forgotten after the first demo.

Keep 26.9 separate from the major-version decision

Node.js 26.9 is a Current release. Its release notes tell you what changed, while a production upgrade decision also depends on support policy, dependencies, native addons, container images, observability, and rollback.

That broader analysis belongs in the Node.js 24 LTS vs 26 Current guide. For 26.9 itself, the practical conclusion is smaller: the release opens several useful paths, but the most interesting ones still carry experimental stability labels. Prototype the exact feature you need, keep the experiment isolated, and let evidence decide whether it belongs in production.

Read the stability marker beside each API again when moving to the next 26.x release. A successful 26.9 prototype is evidence for that pinned build, not a permanent compatibility guarantee for the rest of the Current line.


FAQs

What are the main changes in Node.js 26.9?

Node.js 26.9 adds experimental Web Worker support, integrates mounted virtual file systems with CommonJS and ESM loading, enables the experimental FFI module by default, introduces the experimental node:bench runner, adds crypto provider discovery and a generic MAC API, expands histogram exchange, and introduces an experimental DTLS API.

Are Web Workers stable in Node.js 26.9?

No. Web Worker support is experimental and requires the --experimental-web-worker flag. It should be evaluated for web-compatible libraries and portability, not treated as a replacement for the stable worker_threads API without testing.

Is node:bench stable?

No. The node:bench module has Stability 1.0, Early Development, and requires Node.js to start with --experimental-bench. Its API can change, so benchmark suites should pin the runtime and avoid making the module a permanent public contract.

What changed for the Node.js virtual file system in 26.9?

Mounted VFS paths now work with node:fs, require(), and import(). Node.js 26.9 also adds mount and unmount lifecycle APIs, module-cache invalidation for unmounted VFS instances, ZIP-backed providers, and VFS asset support for single executable applications.

Should production services upgrade from Node.js 24 LTS just for these features?

Usually not without a specific requirement. Node.js 26.9 is a Current release and several headline APIs are experimental. Evaluate the feature in an isolated workload, pin the runtime, measure it, and keep a rollback path.

🚀

Work with us

Let's build something together

We build fast, modern websites and applications using Next.js, React, WordPress, Rust, and more. If you have a project in mind or just want to talk through an idea, we'd love to hear from you.

Related Articles


Nandann Creative Agency

Crafting digital experiences that drive results

© 2025–2026 Nandann Creative Agency. All rights reserved.

Live Chat