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) — eachnew 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
- Troubleshooting — symptom → cause → fix
- Security — threat model and what is out of scope
- Use Cases — when qzjs fits and when it does not