Skip to content

Node.js Compatibility

Thunder ships 42 node:* module polyfills so that npm packages and Node-oriented code can run at the edge without modification. Each module falls into one of three compatibility levels.

LevelMeaning
FullThe module behaves the same as in Node.js for all common use cases.
PartialCore functionality works; some advanced or rarely used APIs may be missing or differ.
StubThe module can be imported without error, but calling its APIs throws a descriptive error at runtime.

Stub modules exist so that transitive dependencies that merely import a module (without calling it on the hot path) do not crash at load time.

When a stubbed API is called, Thunder throws:

[thunder] <api> is not implemented in this runtime profile

For example, require("child_process").spawn(...) throws:

[thunder] child_process.spawn is not implemented in this runtime profile

These modules provide working implementations for the most common operations. Check the Notes column for specifics.

ModuleLevelNotes
node:assertPartialassert, assert.ok, assert.strictEqual, assert.deepStrictEqual, assert.throws, and friends.
node:async_hooksPartialAsyncLocalStorage works. AsyncResource is available. createHook is a no-op.
node:bufferPartialBuffer.from, Buffer.alloc, Buffer.concat, toString encodings. Most methods present.
node:consolePartialDelegates to the global console. Matches standard methods.
node:cryptoPartialrandomBytes, randomUUID, createHash (sha1, sha256, sha384, sha512, md5), createHmac, timingSafeEqual, pbkdf2, scrypt. No cipher streams.
node:diagnostics_channelPartialchannel(), subscribe(), unsubscribe(). No TracingChannel.
node:dnsPartiallookup, resolve4, resolve6, resolveTxt, resolveMx backed by DNS-over-HTTPS (DoH).
node:eventsPartialEventEmitter, once, on. captureRejections supported.
node:fsPartialVFS-backed. readFileSync, writeFileSync, mkdirSync, readdirSync, statSync, existsSync, unlinkSync, renameSync. Async variants delegate to sync under the hood. See Virtual File System.
node:fs/promisesPartialPromise wrappers around VFS-backed sync operations.
node:httpPartialcreateServer is not available (the runtime is the server). request and get for outbound calls work.
node:httpsPartialSame as node:http with TLS.
node:modulePartialcreateRequire works for CJS interop.
node:netPartialOutbound Socket and connect for TCP. No createServer.
node:osPartialplatform, arch, tmpdir, EOL, homedir. Values reflect the runtime sandbox, not the host.
node:pathPartialFull posix implementation. win32 methods are present but always use posix semantics.
node:perf_hooksPartialperformance.now(), PerformanceObserver.
node:processPartialenv, cwd(), nextTick, hrtime, hrtime.bigint, platform, arch, version, exit (terminates the isolate).
node:punycodePartialencode, decode, toASCII, toUnicode.
node:querystringPartialparse, stringify, escape, unescape.
node:streamPartialReadable, Writable, Duplex, Transform, PassThrough, pipeline, finished. Web-stream interop.
node:string_decoderPartialUTF-8, UTF-16LE, Base64, Latin1.
node:timersPartialsetTimeout, setInterval, setImmediate, clearTimeout, clearInterval, clearImmediate.
node:timers/promisesPartialsetTimeout, setInterval, setImmediate returning promises.
node:tlsPartialOutbound connect. No createServer.
node:urlPartialWHATWG URL and legacy url.parse, url.format, url.resolve.
node:utilPartialpromisify, callbackify, inspect, format, types, TextEncoder, TextDecoder.
node:v8PartialgetHeapStatistics returns current isolate heap stats. Serializer/Deserializer are stubs.
node:zlibPartialgzip, gunzip, deflate, inflate, brotliCompress, brotliDecompress. Backed by native Rust ops for performance.

These modules can be imported but all exported functions throw at call time.

ModuleLevelNotes
node:child_processStubProcess spawning is not allowed in the sandbox.
node:clusterStubMulti-process clustering is managed by the runtime, not user code.
node:dgramStubUDP sockets are not available.
node:http2StubHTTP/2 client/server APIs are not exposed; the runtime handles HTTP/2 at the listener level.
node:inspectorStubV8 inspector protocol is not exposed to user code.
node:readlineStubNo interactive terminal in edge functions.
node:replStubNo interactive REPL in edge functions.
node:sqliteStubNo embedded database.
node:testStubNode test runner is not available. Use external test frameworks.
node:vmStubNested V8 contexts are not permitted.
node:worker_threadsStubParallelism is managed by the isolate pool, not user-level threads.

The node:crypto polyfill covers the most common hashing and HMAC operations without pulling in a JavaScript-based fallback. Under the hood it delegates to Deno’s native crypto ops, which use ring/rustls.

import { createHash, createHmac, randomBytes } from "node:crypto";
const hash = createHash("sha256").update("hello").digest("hex");
const hmac = createHmac("sha256", "secret").update("data").digest("base64");
const bytes = randomBytes(32);

File system operations are backed by the Virtual File System. /bundle is mounted read-only from the deployed function bundle; /tmp is writable with quota enforcement.

import { readFileSync, writeFileSync } from "node:fs";
const config = readFileSync("/bundle/config.json", "utf-8");
writeFileSync("/tmp/cache.json", JSON.stringify({ ts: Date.now() }));

DNS resolution uses DNS-over-HTTPS rather than system resolvers. This avoids depending on host /etc/resolv.conf and works consistently across deployment environments. The DoH endpoint is configurable via --dns-doh-endpoint.

import { promises as dns } from "node:dns";
const addresses = await dns.resolve4("example.com");

Compression and decompression are handled by native Rust operations, providing significantly better throughput than pure-JavaScript implementations.

import { gzipSync, gunzipSync } from "node:zlib";
const compressed = gzipSync(Buffer.from("hello world"));
const original = gunzipSync(compressed).toString();

Both ESM and CJS-style imports are supported:

// ESM
import { readFileSync } from "node:fs";
import crypto from "node:crypto";
// CJS (via createRequire or bundler transform)
const fs = require("node:fs");

The node: prefix is required. Bare specifiers like require("fs") are rewritten to node:fs by the module loader.