Starting from Node.js 26.9.0, you can call a function from a C library directly from JavaScript using node:ffi: no flags, no node-gyp, and no native addon compilation. The module appeared in 26.1 behind a flag; what changed on September 16, 2026 is that it’s now enabled by default. It remains marked as experimental and, at the time of writing this note, is only in the Current release line, not in LTS.
What changed in Node.js 26.9?
The release notes for 26.9.0 put it plainly: ffi: enable module by default (Matteo Collina) (nodejs/node#65475). The API is not new — node:ffi was added in v26.1.0 — but until 26.9 you had to enable it manually. We verified this on both sides on a Linux x64 machine: on 26.8.2, import 'node:ffi' fails with ERR_UNKNOWN_BUILTIN_MODULE unless you pass --experimental-ffi; on 26.9.0 the same file runs without any flag.
“Enabled by default” does not mean “stable”. The documentation still marks node:ffi as Stability: 1 – Experimental, each execution prints an ExperimentalWarning, and the module can be disabled with --no-experimental-ffi.
What is FFI and what does it replace in Node.js?
FFI (foreign function interface) is the mechanism that allows one language to call functions compiled in another: in this case, JavaScript calling a C function that lives in a .so, a .dylib, or a .dll. Until now, the standard way to do this from Node was to write a native addon: a wrapper in C/C++, a build step with node-gyp, and a C++ toolchain on every machine that installs your package (or a matrix of precompiled binaries by operating system and architecture), or else resort to a third-party FFI package from npm.
With node:ffi you describe the signature of each function in JavaScript and Node does the marshalling at runtime. There’s nothing to compile.
How do I call a C library from Node.js without compiling anything?
You call dlopen() with the path to the library and an object that describes the argument types and return type of each function. Here are three functions from the C standard library of the system (Linux):
// ffi-demo.mjs
import { dlopen } from 'node:ffi';
{
using libc = dlopen('libc.so.6', {
strlen: { arguments: ['string'], return: 'uint64' },
getpid: { arguments: [], return: 'int32' },
abs: { arguments: ['int32'], return: 'int32' },
});
console.log(libc.functions.strlen('yoDEV')); // 5n
console.log(libc.functions.abs(-42)); // 42
console.log(libc.functions.getpid() === process.pid); // true
} // the library closes automatically here
node ffi-demo.mjs
We ran exactly this code on Node 26.9.0 and got 5n, 42, and true, preceded by the experimental module warning. Three details worth noting:
using: the object thatdlopen()returns implements explicit resource management, so the library handle closes when the block ends. Withoutusing, you close it yourself withlib.close().- 64-bit integers are
bigint.strlenreturns asize_t, declared asuint64, which is why the result is5nand not5. According to the documentation, the same applies to arguments:int64/uint64parameters receivebigint. - Strings are copied. A JavaScript string passed as
stringis copied to a temporary NUL-terminated UTF-8 buffer for the duration of the call. Pointers returned by a function arrive asbigintaddresses.
For your own library, ffi.suffix gives you the platform extension (so, dylib, or dll), so `./mylib.${suffix}` works on all three systems.
The supported types are C primitives (from int8 to uint64, float32, float64, char, bool), plus pointer, string, buffer, arraybuffer, and function for callbacks. Structs are not on the list.
node:ffi vs node-gyp: When do you still need a native addon?
You still need an addon when you have to write your own native code. node:ffi solves a specific case: the C library already exists and you just want to call it. For that, it eliminates the node-gyp step, the C++ compiler on the installer’s machine, and the matrix of precompiled binaries. If instead the logic you need doesn’t exist yet in any compiled library, someone has to write and compile it; FFI doesn’t change that.
Is it safe to use node:ffi?
It’s exactly as safe as C, meaning: not by default. The documentation is straightforward: “This API is unsafe.” If you declare an incorrect signature, pass an invalid pointer, or touch already-freed memory, the process can crash or memory can be corrupted, and nothing on the JavaScript side will stop you. node:ffi doesn’t keep track of pointer validity, memory ownership, or lifetimes. The signature you write is a promise to the runtime, and the runtime believes you.
It also interacts with Node’s Permission Model. With --permission, any FFI call throws ERR_ACCESS_DENIED unless you add --allow-ffi, and when you do, Node prints a SecurityWarning alerting you that the flag “could invalidate the permission model”. It’s accurate: native code runs outside all the sandboxing that the permission model offers.
Does node:ffi work on Windows?
Yes, with limits that the documentation details:
dlopen(null)— loading symbols from the process itself instead of a file — is not supported on Windows.- On Windows x64, the fast path supports a maximum of 3 arguments; functions with more fall back to the generic path, which is slower.
- The module only exists in builds with FFI support. The included
libffidoesn’t supports390x, among a few other targets.
On Linux and macOS (x86-64), the fast path supports up to 6 integer or pointer arguments and 8 floating-point ones.
How do you benchmark in Node.js with node:bench?
With node:bench, the built-in benchmarking runner that also landed in 26.9, by James M Snell (nodejs/node#65606). Unlike FFI, it doesn’t come enabled by default: a later commit in the same release put it behind --experimental-bench, and it’s marked as Stability 1.0 – Early Development. Without the flag, node --bench refuses to run.
Here’s a benchmark of the strlen call from above, alongside JavaScript’s native .length:
// bench.mjs
import { dlopen } from 'node:ffi';
import { bench, suite } from 'node:bench';
const { functions } = dlopen('libc.so.6', {
strlen: { arguments: ['string'], return: 'uint64' },
});
const input = 'https://www.yodev.dev/';
suite('string length', () => {
bench('strlen via node:ffi', (b) => {
const ops = 100_000;
let total = 0n;
b.start();
for (let i = 0; i < ops; i++) total += functions.strlen(input);
b.end(ops);
if (total !== BigInt(ops * input.length)) throw new Error('unexpected result');
});
bench('String.prototype.length', (b) => {
const ops = 100_000;
let total = 0;
b.start();
for (let i = 0; i < ops; i++) total += input.length;
b.end(ops);
if (total !== ops * input.length) throw new Error('unexpected result');
});
});
node --experimental-bench --bench bench.mjs
The pattern follows the documentation: b.start() and b.end(ops) delimit the measured region, and the final check makes the result observable so the engine can’t eliminate the work during optimization. By default, each file runs in its own child process, and the spec reporter prints the samples, the mean rate, a 95% confidence interval, and the median.
On our test machine, the FFI call ran at about 7 million calls per second, and .length was two orders of magnitude faster. Take that second figure with care: a loop that reads the length of a string is exactly the kind of work that the JIT can simplify, and the node:bench documentation warns about it explicitly. The lesson holds all the same: every FFI call has a real fixed cost. node:ffi is worth it when the C function does significant work on each call—compression, image decoding, a system API—not when you call it millions of times for something JavaScript already does. If you’re interested in measuring performance at project scale, at yoDEV we have a practical guide to performance with Turborepo.
Is node:ffi in Node.js LTS? Should you use it today?
No: at the time of publishing this note, node:ffi is only enabled by default in the Current line (26.x); the current LTS is 24.x. For scripts, internal tools, and prototypes on Node 26, use it: it’s the shortest path between “there’s a C library for this” and calling it. For a package published on npm, not yet. It’s experimental, it’s not in any LTS line, and whoever installs your package on an earlier Node is going to get ERR_UNKNOWN_BUILTIN_MODULE. The version numbers in this note will age; the fact that Node core now includes FFI, won’t.