Skip to content

Architecture ​

Everything that decides what happens to a request (URL handling, validation, retries and backoff, per-attempt timeouts, typed errors, interceptors) is Rust compiled to WebAssembly, so it behaves the same in every runtime. Underneath, a transport only moves bytes:

TransportWhereIn the npm package
The platform's fetch()Browsers and Web WorkersYes
XMLHttpRequestBrowsers, for upload progressYes
node:http / node:https with keep-aliveNode.jsYes
The runtime's WebSocket, with the connection policy (URL, timeout, reconnection, errors) in RustWebSockets, everywhereYes
Native addon: HTTP/1.1 on hyper and Tokio threads, loaded through Node-API, with the same redirect, decompression, timeout and connection-limit rules and Node's TLS defaults (Mozilla root certificates, NODE_EXTRA_CA_CERTS, NODE_TLS_REJECT_UNAUTHORIZED, with Node's warning when that turns verification off)Node.jsNot yet: build it with npm run build:native

Each transport's own behaviour (redirects, decompression, limits, cancellation, errors, streaming, the security checks) is held to the same results by a conformance suite: the same 52 behaviours run on node:http, the native addon, fetch() in Node.js, Chromium, Firefox and WebKit, and CI fails on any difference that isn't documented. The results, and the documented differences, are in CONFORMANCE.md.

Which transport a request uses:

RequestBrowsersNode.js
Most requestsfetch()The native transport where it's built and Node's global agents are untouched; otherwise node:http
With onUploadProgress (with or without onDownloadProgress)XMLHttpRequest, or fetch() where there's nonenode:http
With onDownloadProgress only, responseType: 'stream' or 'ndjson', and client.events()fetch()node:http (since 2.4.0; it was Node's fetch() before)
With the proxy option, or allowPrivateNetwork: falsefetch() (both are Node.js options)node:http

What the native transport changes. Connection handling, TLS, HTTP parsing and decompression move from the JavaScript event loop to Tokio threads; JavaScript only builds requests and decodes their bodies. That's why it gains most with many requests in flight, and why the hand-off between threads likely contributes to its slower sequential results (measurements).

When it's used. The native transport is used when rust-fetch-native.<platform>-<arch>.node sits next to the package's node.js (npm run build:native puts it there). It steps aside for the requests in the table above, and for every request where you have installed your own http.globalAgent / https.globalAgent (for example a proxy agent) or set NODE_USE_ENV_PROXY; RUST_FETCH_NATIVE=0 turns it off.

Connection pooling ​

Browsers pool connections for fetch(). In Node.js, both transports keep up to 256 keep-alive connections per origin and queue further requests until one is free, so bursts reuse connections instead of exhausting local ports. If you install your own http.globalAgent / https.globalAgent, or set limits on Node's, that agent is used instead.

Released under the MIT or Apache-2.0 license.