node-tls-client binary
Our build of tls-client runs requests without holding a thread, so throughput is no longer capped at the thread count.
By default, initTLS() downloads tls-client's official release. binary: "node-tls-client" downloads our build instead: the same tls-client code, with two additions that let a request wait for its server without occupying a thread.
// Our build of tls-client: a request holds no thread while it waits.
await initTLS({ binary: "node-tls-client" });Nothing else in your code changes: the API, browser profiles, options and responses are the same.
Why it's faster
With the official binary, each request holds a libuv threadpool thread until its response arrives, so requests running at once are capped at UV_THREADPOOL_SIZE (CPU cores × 2 by default). Against a server that takes 50 ms, 20 threads finish at most 400 requests per second, however many you start.
Our build starts the request inside the library and returns at once. Waiting for the network happens there, the way node-fetch waits inside Node, and the library calls back when responses arrive. No thread is held per request, so every request you start runs at once:
const session = new Session({ timeout: 15_000 });
const urls = Array.from(
{ length: 20 },
(_, i) => `https://httpbin.org/anything/${i}`,
);
// Every request starts right away, however many there are, like node-fetch.
const responses = await Promise.all(urls.map((url) => session.get(url)));
console.log(responses.map((r) => r.status)); // [ 200, 200, … ]| Official binary (default) | binary: "node-tls-client" | |
|---|---|---|
| Requests running at once | up to UV_THREADPOOL_SIZE | every request you start |
UV_THREADPOOL_SIZE | sets the speed limit | doesn't matter |
initTLS({ concurrency }) | caps requests in flight | ignored, with a warning |
| tls-client version | newest official release (1.18.1) | newest upstream version tag (1.18.1) |
Against a server that answers in 50 ms, 64 requests in flight reach 1,207 requests per second instead of 385, and 5,000 started at once finish in 0.26 s instead of 13 s. Charts and all results are on Performance.
Memory
Like node-fetch, nothing limits how many requests are in flight, so memory grows with them: peak memory was about 190 MB with 1,024 requests in flight and 350 MB with 5,000 started at once, close to node-fetch (180 MB and 280 MB). To bound it, limit requests in flight in your own code, for example with a small pool or p-limit, as you would with node-fetch.
When it falls back
initTLS() uses the official binary instead, with a warning, when:
- our release has no build for your platform, or the download fails and no copy is cached;
- you call it from a worker thread. The faster mode works on the main thread only: the library has one completion queue per process, so threads would collect each other's responses. In a worker, each request holds a threadpool thread, as with the official binary.
The library is loaded once per process. To switch between the two binaries, restart the process.
Worker threads and in-flight requests
With either binary, terminating a worker thread while one of its requests is
still in flight can crash the whole process. Await or close the worker's
requests before calling worker.terminate().
What differs from upstream
Our build lives at Sahil1337/tls-client, a fork of tls-client. The only changes are two files in cffi_dist, the folder that turns tls-client into a shared library:
| Added | What it does |
|---|---|
async.go | requestAsync starts a request and returns at once; waitNext collects finished ones. |
notify.go | setNotify lets the library call node-tls-client back when requests have finished. |
Everything else, including the TLS fingerprints and the blocking request call the official binary uses, is upstream's code at the tagged version.
How it's built and released
- Versions follow upstream. Each upstream version tag is merged and released as
v<version>-nodetls.<n>, e.g.v1.18.0-nodetls.1. A daily job checks for new upstream tags. - Built in public CI. GitHub Actions builds every platform from the tagged source, then loads each build on Linux x64 and arm64, macOS arm64 and Windows x64 and runs real requests through it before publishing.
- Platforms: the same as upstream's builds. macOS x64 and arm64; Linux x64, arm64, armv5/6/7, ia32, ppc64le, riscv64 and s390x (glibc); Windows x64 and ia32.
- Checksums and licenses. Each release includes
SHA256SUMSandTHIRD_PARTY_NOTICES.txt(tls-client is BSD-3-Clause). To verify a downloaded file:
sha256sum -c SHA256SUMS --ignore-missingnode-tls-client downloads the newest release into the same cache directory as the official binary, under its own file name.