
Node.js 26.1.0 landed on May 7 with something the community has been asking for — and watching Deno and Bun enjoy — for years: a built-in Foreign Function Interface. The new node:ffi module lets you load native C shared libraries and call their symbols directly from JavaScript. No C++. No node-gyp. No binding.gyp to maintain. Deno had this in 2021. Bun shipped it at launch. Node.js just closed the gap.
What node:ffi Does (and Why It Took This Long)
A Foreign Function Interface lets one language call code written in another. In Node.js’s case, that means calling functions in .so files (Linux), .dylib files (macOS), or .dll files (Windows) — the native shared libraries your OS and C ecosystem are built on.
Before node:ffi, your options were: write a C++ addon using N-API (requires a compiler, a build step, and knowing C++), or use the community node-ffi-napi package (requires compiling libffi, historically under-maintained). Neither is pleasant when you just want to call one function in a vendor library.
Issue #46233 on the Node.js repo asked for built-in FFI years ago and was closed as “not planned.” The team’s position shifted — arguably pushed by Deno and Bun making FFI a table-stakes feature. Now it’s in core.
Getting Started: The Basic Pattern
Enable the module with --experimental-ffi. Then use dlopen() to load a library and declare the functions you want to call:
// node --experimental-ffi script.js
const { dlopen, suffix } = require('node:ffi');
// suffix is 'dylib' on macOS, 'so' on Linux, 'dll' on Windows
const { functions } = dlopen(`libm.${suffix}`, {
cos: { parameters: ['f64'], result: 'f64' },
pow: { parameters: ['f64', 'f64'], result: 'f64' },
});
console.log(functions.cos(0)); // 1
console.log(functions.pow(2, 10)); // 1024
You declare each function’s parameter types and return type. The module handles type conversion between JavaScript and native. Supported types cover the full integer range (i8 through i64/u64), floats (f32, f64), and pointer-like types (pointer, string, buffer).
The DynamicLibrary Class: More Control
For more complex integrations, use the DynamicLibrary class directly. Here’s calling sqlite3_libversion without any npm driver:
const { DynamicLibrary, suffix } = require('node:ffi');
const lib = new DynamicLibrary(`libsqlite3.${suffix}`);
const sqlite3_libversion = lib.getFunction('sqlite3_libversion', {
parameters: [],
result: 'string',
});
console.log(sqlite3_libversion()); // "3.53.0"
lib.close();
You can also pass JavaScript callbacks to native code. Register a JS function as a native-callable pointer:
const comparator = lib.registerCallback(
{ parameters: ['i32', 'i32'], result: 'i32' },
(a, b) => a - b,
);
// comparator is a bigint pointer — pass it to native qsort() or similar
Memory helpers cover the full range: getInt32(), setInt32(), toString() for C strings, toBuffer() and toArrayBuffer() for raw memory access.
The Safety Warning Is Not Optional
The Node.js documentation calls this API “inherently unsafe.” That language is deliberate.
Pass an invalid pointer: process crash. Declare a function with the wrong signature: memory corruption or crash. Access memory after it’s been freed: undefined behavior. These aren’t edge cases — they’re what happens when JavaScript loses its safety net. You are, in effect, writing C through a JavaScript door.
- Never use zero-copy buffer views (
copy: falseintoBuffer()) after the native memory is released - Callbacks must run on the same thread they were created on
- Callbacks cannot throw exceptions or return promises
- Don’t resize or transfer Buffer backing stores during active native calls
If you’re not comfortable reasoning about pointer lifetimes, use copy: true (the default) everywhere and keep callback lifetime management explicit.
When to Use node:ffi (and When Not To)
node:ffi is the right tool when you need to wrap a vendor .so you don’t control, access OS system libraries, or prototype a native integration before committing to a full C++ addon. IoT and embedded work — where you have a hardware driver as a shared library — is a natural fit.
It is not the right tool for hot paths. FFI calls carry non-trivial overhead. N-API C++ addons link directly without an FFI dispatch layer and are significantly faster for high-frequency native calls. If you’re calling a native function millions of times per second, write the addon.
| Approach | Build step? | C++ needed? | Performance | Best for |
|---|---|---|---|---|
node:ffi (built-in) | No | No | Moderate | Wrapping vendor libs, prototyping, occasional calls |
node-ffi-napi (npm) | Yes | No | Similar | Legacy projects only — unmaintained |
| N-API / node-addon-api | Yes | Yes | Best | High-frequency native calls, production |
What to Watch
node:ffi is Stability 1 — experimental. The API will change before it stabilizes. Node.js 26 is the “Current” branch, not LTS. The LTS designation for Node.js 26 is expected later in 2026, which will be the first realistic signal for production adoption.
The Permission Model integration (--allow-ffi) is notable: when Node.js’s sandbox mode is active, FFI access is explicitly gated. That’s the right design for a capability this powerful.
For now: use it in development, test environments, and scripts where a process crash is recoverable. Build something. Learn the API. By the time it hits LTS, you’ll know exactly how to deploy it.













