Node.js Compatibility
See which Node.js features nstd supports, what is missing, and how compatibility is tested.
nstd runs Node programs without Node installed. This page explains how closely it follows real Node, what works, what doesn't, and what happens when your program uses something nstd can't do.
#Which Node version?
nstd's standard library is built from Node's own source code, pinned to
v26.10.0. The JavaScript behind fs, path, stream, url and the rest
isn't a reimplementation. It's Node's own code, adapted to run without Node
underneath. Where a module's logic matches Node v26.10.0, that's because it
is the same code.
That doesn't mean nstd behaves like Node v26.10.0 in every way. Whenever Node's standard library needs the operating system (files, sockets, encryption, processes), it calls into C++. nstd answers those calls with its own host, written in Zig, using different libraries: QuickJS-NG instead of V8, and Mbed TLS instead of OpenSSL. Where the host provides a feature, Node's JavaScript runs unchanged on top of it. Where it doesn't, the module still loads, but the specific call that needs the missing piece throws an error. See What doesn't work below.
#What works
A large part of the standard library behaves like the real thing, including file and network access:
- Core utilities.
path,util,events,stream,buffer,string_decoder,querystring,punycode,assert,urland theURL/URLSearchParamsglobals all run Node's own code and are checked against real Node's output. - The real filesystem.
fsandfs/promiseswork on the actual disk.fs.watch()(includingrecursive: true) andfs.watchFile()both work. require()andimport(). Both module loaders are Node's own, includingnode_moduleslookup,"exports"inpackage.json, JSON files and.mjsfiles.- Randomness, WebCrypto and hashing.
crypto.randomBytes(),randomUUID(),randomInt(),timingSafeEqual()and all ofcrypto.subtlework.createHash(),createHmac()andcrypto.hash()support fifteen algorithms, computed by Mbed TLS or, forsha512-224,sha512-256,shake128andshake256, in JavaScript. - DNS.
dns.lookup()and thedns.resolve*()functions return real answers. - Networking and TLS.
net.connect(),http.request(),https.request()andfetch()open real connections, including TLS. Servers work too:net,httpandhttpscanlisten()on TCP and Unix domain sockets. - Child processes.
spawn,exec,execFile,forkandclusterall start real processes. - Worker threads.
new Worker()starts a real thread with its own JavaScript engine. - Compression.
gzip,deflate,deflateRawandunzipwork, both as one-shot functions and as streams, and so doeszlib.crc32().brotliDecompressworks too, so HTTP responses compressed withgziporbrare decoded like in any other client. - Machine information.
os.hostname(),platform(),arch(),homedir(),tmpdir(),totalmem(),freemem(),uptime(),userInfo(),networkInterfaces()and, on Linux,cpus()all report the real machine.
#What doesn't work
Some features aren't built yet, or have no equivalent without the libraries Node uses:
- Crypto that needs OpenSSL. Ciphers, key objects, signatures,
Diffie-Hellman and X.509 certificates rely on OpenSSL, which nstd doesn't
include.
crypto.getCiphers()returns[]. (Hashing isn't affected; see above.) - TLS servers.
tls.connect()andhttps.request()work as clients, buttls.createServer()andhttps.createServer()throw. For the same reason, TLS session resumption isn't available. - Brotli compression and Zstandard.
zlib.brotliCompress(),zlib.zstdCompress()andzlib.zstdDecompress()throw. Everything DEFLATE based works, and so does Brotli decompression, which is what an HTTP client needs. - Sending a handle to a child process.
subprocess.send(message, handle)throws. Everything else about child processes works. os.loadavg(). Not available.- Non-UTF-8 filenames.
fs.watch()andfs.readdir()throw if you ask forbuffer,hex,latin1orbase64filename encodings, instead of quietly returning strings. - The exact error in an unhandled rejection. An unhandled rejection stops
the program and
process.on("unhandledRejection", ...)fires, just like in Node. But when the rejection reason is anError, nstd reports it wrapped in anUnhandledPromiseRejectionobject instead of as the original error. This happens because QuickJS-NG stores an error's stack differently from V8. The exit code and the message are correct; only the reported object differs.
For the complete picture, module by module, see the API support pages.
#When something isn't supported
When you call something nstd can't do, it never returns undefined or fails
silently. It throws an error that names exactly what's missing, with
error.code set to ERR_NSTDLIB_NO_BINDING. For example,
crypto.createCipheriv() throws:
Error: [nstdlib] internalBinding('crypto').CipherBase() is not implemented:
this runtime has no such binding, and no host implementation is installed on
`globalThis.node_binding_crypto_CipherBase`.TLS server features throw the same way, with the code
ERR_NSTDLIB_NO_OPENSSL, so you can tell the two cases apart.
This is deliberate. Earlier versions returned undefined instead, and that
caused subtle bugs. undefined looks like a real answer, so code kept
running with the wrong value. timingSafeEqual() treated every comparison as
unequal, and a dns.lookup() call never called back, so the program hung
without an error.
A thrown error shows up immediately, with a stack trace pointing at the call that failed. If your program doesn't behave as expected, look for one of these errors first. A missing feature that announces itself is much easier to work around than one that quietly gives the wrong answer.
#How compatibility is tested
The claims on this page are checked in two independent ways, both based on Node itself rather than on a description of Node:
- Node's own test suite. Thousands of files from Node's
test/parallel, the most complete description of how each builtin should behave, run directly on nstd with no Node underneath. - Side-by-side tests. The same program runs under real Node and under nstd, and the two outputs are compared byte for byte. These cover URL parsing, networking, HTTP parsing, TLS, child processes, the filesystem and more.
The results change as the project moves, so this page doesn't repeat them. The API support pages show the current status of every module: which ones match Node's behavior, which ones only have the right shape and throw when called, and which ones haven't been tested yet.
You can also check one number yourself: nstd --print-realm shows how many
builtin modules loaded at startup, and lists any that didn't.