Skip to content

Add http/https vs fetch transport benchmark for the Node.js client#779

Draft
Copilot wants to merge 6 commits into
mainfrom
copilot/replace-http-https-modules
Draft

Add http/https vs fetch transport benchmark for the Node.js client#779
Copilot wants to merge 6 commits into
mainfrom
copilot/replace-http-https-modules

Conversation

Copilot AI commented May 29, 2026

Copy link
Copy Markdown
Contributor

Summary

Issue #511 proposes replacing the Node.js client's legacy http/https transport with fetch/undici for performance, and asks for trustworthy comparison data before committing to a rewrite. This PR adds a reproducible benchmark that pits the current client against a trivial undici.request() stub over identical requests — no transport rewrite yet, just the data to justify one.

  • benchmarks/transport/ — new benchmark:
    • clients.ts — a TransportClient abstraction with two impls driven through the same scenarios: SdkTransportClient (@clickhouse/client as built from this repo, uses exec() to isolate transport from row parsing) and UndiciTransportClient (minimal undici.request() stub).
    • index.ts — runner for three use cases with warmup + configurable iterations and p50/p90/p99 latency: single-request latency (SELECT 1), download throughput (large result set), upload throughput (POST to null() so no table setup is needed). Tunable via CLICKHOUSE_URL, LATENCY_REQUESTS, DOWNLOAD_ROWS, UPLOAD_ROWS, ITERATIONS, WARMUP.
    • stats.ts — dependency-free latency/throughput helpers.
    • README.md — run instructions, config, interpretation, sample local run.
  • Adds undici as a devDependency (Node bundles it internally but does not expose it as an importable module).

Why undici.request() and not the global fetch()?

An earlier revision of this benchmark used fetch() and showed it losing the download scenario by ~5×. That gap turned out to be entirely the WebStreams (ReadableStream) layer that fetch() routes the response body through — a known Node-core bottleneck (nodejs/undici#1203) — not a property of undici. undici.request() returns a native Node Readable (the same stream type @clickhouse/client drains), which is the API a real migration would actually adopt and makes this an apples-to-apples transport comparison.

Indicative results

Single local run, macOS / Apple Silicon, Node v24.6.0, ClickHouse head, loopback, default config (LATENCY_REQUESTS=200 DOWNLOAD_ROWS=1000000 UPLOAD_ROWS=1000000 ITERATIONS=10 WARMUP=3), stable across two runs:

Scenario @clickhouse/client (http/https) undici.request() stub Winner
SELECT 1 latency (mean) ~2.1–2.7 ms ~0.50 ms undici ~4–5×
Download throughput ~1310–1440 MiB/s ~1850–1920 MiB/s undici ~1.3–1.4×
Upload throughput ~223–225 MiB/s ~241–245 MiB/s undici ~tied (server-bound)

With native streams, undici.request() is faster or tied across the board — a much clearer signal in favour of the migration than the earlier fetch()-based numbers suggested. Caveats: this is a bare transport stub vs the full client (the stub omits settings/retries/compression/keep-alive tuning/abort/logging), and these are loopback numbers — a remote/cloud endpoint via CLICKHOUSE_URL would narrow the relative gaps. Re-run per workload.

docker-compose up -d
npm run build                              # so @clickhouse/client resolves at runtime
npx tsx benchmarks/transport/index.ts

Checklist

  • A human-readable description of the changes was provided to include in CHANGELOG

@CLAassistant

CLAassistant commented May 29, 2026

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ peter-leonov-ch
❌ Copilot
You have signed the CLA already but the status is still pending? Let us recheck it.

Copilot AI changed the title [WIP] Replace legacy http/https with fetch API for improved performance Add http/https vs fetch transport benchmark for the Node.js client May 29, 2026
Copilot AI requested a review from peter-leonov-ch May 29, 2026 23:25
peter-leonov-ch and others added 4 commits June 12, 2026 23:53
The benchmark drives `@clickhouse/client` as built from this repository
(via the npm workspace symlink), not the published npm release — fix the
misleading wording in the README and clients.ts.

Replace the broken `tsc --project ... && node ...` recipe (the `src` path
alias breaks tsc's rootDir and node globals are unresolved) with
`npm run build` + `npx tsx`, and add the build prerequisite so
`@clickhouse/client` resolves at runtime.

Refresh the sample-results table with real numbers measured locally
(macOS/Apple Silicon, Node v24.6.0, ClickHouse head, default config).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rison

The fetch() stub routed the response body through the WebStreams
(ReadableStream) layer, a known Node-core bottleneck (nodejs/undici#1203)
that drains large bodies several times slower than native streams. That
made the previous "http/https wins download by ~5x" result an artifact of
WebStreams, not a property of the undici transport a real migration would
adopt.

Switch the stub to undici.request(), which returns a native Node Readable
drained the same way as @clickhouse/client — an apples-to-apples transport
comparison. With native streams undici is faster across the board locally
(latency ~4-5x, download ~1.3-1.4x, upload ~tied/server-bound). Refresh the
README sample table, rationale, and caveats accordingly, and add undici as
a devDependency.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The benchmark cited issue #418, which is actually "Avro not supported".
The transport-replacement proposal it answers is #511.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@peter-leonov-ch

Copy link
Copy Markdown
Collaborator

Benchmark findings: use undici.request(), not fetch()

Updated this benchmark to compare against undici.request() instead of the global fetch() — and it materially changes the conclusion.

The earlier fetch()-based stub lost the download scenario by ~5×, which initially read as "http/https is much faster at streaming large results". That gap turned out to be entirely the WebStreams (ReadableStream) response-body layer that fetch() routes through — a known Node-core bottleneck (nodejs/undici#1203) — not a property of the undici transport. undici.request() returns a native Node Readable (the same stream type @clickhouse/client drains), which is the API a real migration would actually adopt.

Indicative local run (macOS/Apple Silicon, Node v24.6.0, ClickHouse head, loopback, default config), stable across two runs:

Scenario @clickhouse/client (http/https) undici.request() stub
SELECT 1 latency (mean) ~2.1–2.7 ms ~0.50 ms (~4–5× faster)
Download throughput ~1310–1440 MiB/s ~1850–1920 MiB/s (~1.3–1.4× faster)
Upload throughput ~223–225 MiB/s ~241–245 MiB/s (~tied, server-bound)

With native streams, undici (low-level API) is faster or tied across the board — supporting the "or better yet with undici directly" framing in #511. Caveats unchanged: this is a bare transport stub vs the full client (omits settings/retries/compression/keep-alive tuning/abort/logging), and loopback numbers will overstate the gaps vs a remote/cloud endpoint. Re-run per workload.

@codecov

codecov Bot commented Jun 12, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Performance] Replace legacy http/https modules with fetch API or better yet with undici low level api for better performance

3 participants