Skip to content

FAQ ​

Design questions that look like bugs but are not, plus the honest limits. Each answer links to the page that documents the behavior in full.

Positioning ​

Is qzjs a Node.js replacement? ​

No. qzjs is an embeddable runtime for trusted script, not a Node.js substitute. There is no require, no import of Node built-ins, no process, no Buffer, and no CommonJS. Many pure-JS npm packages do work unchanged because they only use the standard Web API surface; Node-only ones do not. Run python3 test/compat_check.py <pkg> to test a specific package — see Compatible Packages.

Is qzjs a browser? ​

No. It implements the WinterTC subset of Web APIs (fetch, WebSocket, streams, crypto, timers, ...), but has no DOM: no document, no window, no rendering. It is a server-/edge-side runtime.

Is qzjs a sandbox for untrusted code? ​

No. qzjs runs trusted script. Script can read and write any path the host process can, spawn processes, and read the full environment — those are legitimate capabilities, not escapes. See Security for the explicit threat model.

How big is it, and how fast does it start? ​

About 2.45 MiB stripped in the minimal profile, with sub-5 ms startup and peak RSS near 3 MB. It is built to fit embedded and edge targets where Node.js or bun cannot. Numbers come from the project's own benchmarks — see Performance Benchmarks.

Concurrency & the Process Model ​

Is it multi-threaded? Can JS run in parallel? ​

The main runtime is single-threaded. All JS runs on qzjs's own internal thread, which also drives the embedded libuv loop; the host thread never calls into JS. Concurrency comes from async I/O, not parallel JS in the main context.

Web Workers do run in parallel — as threads or child processes depending on the backend — but communicate via structured-clone messages.

What is the difference between THREAD and ISOLATED? ​

QZ_PROCESS_MODEL selects the default worker backend:

  • ISOLATED (default) — each new Worker(...) runs in a dedicated child process (qzjs-rt) via fork+exec. Stronger isolation, IPC overhead.
  • THREAD — workers are threads in one process. Lower overhead, shared address space.

Neither is a security boundary against malicious script. See Multi-Context & Web Workers.

Why does a Worker need a companion binary? ​

Under ISOLATED, workers are separate processes, so they need an executable to launch: qzjs-rt. It is built alongside the CLI (QZ_BUILD_CLI=ON). If it cannot be found the spawn fails — see Troubleshooting.

Can workers share memory? ​

No. Messages are structured-cloned, not shared. Use postMessage / MessageChannel.

APIs ​

Which Web APIs are available? ​

21 modules are exposed as globals: fetch, console, crypto.subtle, streams, timers, URL, TextEncoder/TextDecoder, AbortController, WebSocket, BroadcastChannel, EventSource, CacheStorage, Service Worker, Worker, fs, storage, navigator, serve(), grpc, compression, structuredClone, and more. The full list with global names is the JS API Reference.

Why is serve() not on node:http? ​

serve() is qzjs's own HTTP/WS/gRPC server, not a port of Express or node:http. It is a single-server, callback-driven API. Only one server may run at a time. See serve().

Is there a public bytecode API? ​

Yes. qz_compile() compiles JS source to a bytecode blob, and hosts run it at startup via qz_config_t.initial_bytecode (CLI: qzc / qzjs --bytecode). The catch: bytecode is bound to the exact qzjs build and is not portable across versions — the runtime rejects incompatible blobs explicitly. Compile at deploy time for the target build. See Bytecode Compilation.

Can the host call JS directly, e.g. qz_eval? ​

No. There is no qz_eval on the public C API. The host and runtime communicate only over JSON messages — qz_post_message inbound, message_cb outbound. This keeps the JS execution boundary explicit and the host free of an eval channel. See Host Integration.

Why do timers behave oddly under tests? ​

The GoogleTest harness links against mock_libuv, a deterministic in-process fake of the libuv API. Under it, timers are quantized to a 1-second tick so tests advance time deterministically. This is a test-only artifact, not runtime behavior. See Testing.

Build & Profiles ​

What is the difference between minimal and standard? ​

QZ_PROFILE=minimal keeps WebAssembly, crypto.subtle, atob/btoa, and compression — the smallest build that still satisfies the full WinterTC required set. standard adds the remaining QZ_WITH_* features. Individual QZ_WITH_* options override either profile. See Build Options.

Can I enable both WASM engines? ​

No — WAMR and wasm3 both register the WebAssembly global, so they are mutually exclusive. WAMR (Fast Interp + AOT) is the default.

Does qzjs need system libraries? ​

No. Every dependency — libuv, mbedTLS, miniz, WAMR, cJSON, and the rest — is built from source via CMake, from pinned submodules under deps/. Check out submodules recursively or the configure step fails with a clear message.

Why C99? ​

qzjs is embeddable in C99 host applications — device firmware and edge services — and builds as C99 alongside its dependencies. That constraint keeps the runtime buildable in toolchains that predate C11 atomics support.

Errors & Debugging ​

Why did qz_create return NULL? ​

The initial script threw, or thread/loop init failed. See Troubleshooting.

How do I debug JS? ​

Build with debugging and qzjs attaches a DAP debug adapter over stdio automatically — breakpoints, step-through, variable inspection, async-across- pause. There is no CDP/Chrome DevTools support. See Debugging.

Something is wrong with the network stack ​

Check the proxy environment variables first: fetch honors HTTP_PROXY / HTTPS_PROXY / NO_PROXY transparently at the C layer. An unsupported proxy scheme (e.g. socks5://) makes the request fail closed rather than bypass the proxy silently.

See Also ​

MIT Licensed