
# 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](#what-doesnt-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`, `url` and the
  `URL`/`URLSearchParams` globals all run Node's own code and are checked
  against real Node's output.
- **The real filesystem.** `fs` and `fs/promises` work on the actual disk.
  `fs.watch()` (including `recursive: true`) and `fs.watchFile()` both work.
- **`require()` and `import()`.** Both module loaders are Node's own,
  including `node_modules` lookup, `"exports"` in `package.json`, JSON files
  and `.mjs` files.
- **Randomness, WebCrypto and hashing.** `crypto.randomBytes()`,
  `randomUUID()`, `randomInt()`, `timingSafeEqual()` and all of
  `crypto.subtle` work. `createHash()`, `createHmac()` and `crypto.hash()`
  support fifteen algorithms, computed by Mbed TLS or, for `sha512-224`,
  `sha512-256`, `shake128` and `shake256`, in JavaScript.
- **DNS.** `dns.lookup()` and the `dns.resolve*()` functions return real
  answers.
- **Networking and TLS.** `net.connect()`, `http.request()`,
  `https.request()` and `fetch()` open real connections, including TLS.
  Servers work too: `net`, `http` and `https` can `listen()` on TCP and Unix
  domain sockets.
- **Child processes.** `spawn`, `exec`, `execFile`, `fork` and `cluster` all
  start real processes.
- **Worker threads.** `new Worker()` starts a real thread with its own
  JavaScript engine.
- **Compression.** `gzip`, `deflate`, `deflateRaw` and `unzip` work, both as
  one-shot functions and as streams, and so does `zlib.crc32()`.
  `brotliDecompress` works too, so HTTP responses compressed with `gzip` or
  `br` are 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()` and `https.request()` work as clients, but
  `tls.createServer()` and `https.createServer()` throw. For the same reason,
  TLS session resumption isn't available.
- **Brotli compression and Zstandard.** `zlib.brotliCompress()`,
  `zlib.zstdCompress()` and `zlib.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()` and `fs.readdir()` throw if you ask
  for `buffer`, `hex`, `latin1` or `base64` filename 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 an `Error`, nstd reports it wrapped
  in an `UnhandledPromiseRejection` object 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](/api).

## 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](/api) 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.
