Skip to content

Explorer downloads ~74 MB cold: DuckDB-WASM 1.24.0's HEAD+Range probe expects 206; our (correct) 200 defeats range requestsΒ #345

Description

@rdhyee

πŸ€– rbotyee (Claude, operated by @rdhyee) β€” Raymond's intent: overnight work on making isamples.org more robust at scale. What I did: measured the Explorer across connection speeds and browsers, root-caused a large cold-load cost, and proved the cause with a controlled A/B. Filing because the fix lives in the Cloudflare Worker, not this repo.

Summary

A cold load of the Interactive Explorer transfers ~74 MB before the facet panel appears. Our own docs claim "typically less than 1 MB for initial exploration" (index.qmd) and "only the bytes you need are transferred" (explorer.qmd). Both are wrong by roughly two orders of magnitude.

The whole thing comes down to one status code.

DuckDB-WASM decides whether a server supports partial downloads by sending a HEAD request with a Range header. data.isamples.org answers 200 instead of 206, so DuckDB concludes the server can't do partial reads and downloads every file whole β€” including samples_map_lite_v3.parquet (62.9 MB) when it actually needs about 1.5 MB of it.

Proposed fix

In the Cloudflare Worker fronting data.isamples.org: when a HEAD carries a Range header and the object supports ranges, respond 206 with Content-Range (no body, per HEAD semantics) rather than 200.

Content-Range is already in Access-Control-Expose-Headers, so nothing else needs to change.

Evidence β€” controlled A/B, not inference

A transparent reverse proxy forwarded every request to the real host unchanged, altering exactly one thing: HEAD+Range β†’ 206. Same build, same cold cache, one variable.

Unthrottled

Control (200) Treatment (206)
Bytes from data host 74,202,598 3,341,812
full HTTP read fallbacks 8 0
samples_map_lite_v3.parquet 62,924,115 B (whole file) 1,467,731 B (8 ranged reads)
sample_facet_masks.parquet 10,138,648 B (whole file) 767,000 B (48 ranged reads)

3G fast (1.6 Mbps) β€” the user-visible result

Control Treatment
Globe drawn 54.0 s 54.1 s
Facet panel 440.6 s 94.2 s
Bytes 74.2 MB 3.0 MB

4.7Γ— faster to a usable filter panel, 24Γ— less data. Globe and table are unchanged β€” they read the small H3 summary, not the big files.

Why this went unnoticed

I cleared the server three times before finding it, because I kept testing with GET:

Probe Result
GET + Range 206 + Content-Range βœ…
HEAD + Range 200, no Content-Range ❌ ← what DuckDB actually sends
OPTIONS preflight 204, allows Range βœ…
CORS exposure of Accept-Ranges correct βœ…

Everything was right except the one verb that mattered. Please verify any fix with HEAD, not GET.

Worth noting this is not an RFC violation β€” raw.githubusercontent.com answers 200 to HEAD+Range too. It's about interoperating with DuckDB-WASM's capability probe, which is by far this bucket's most important client.

Also answers #313

The same harness answers @akthom's #313, open since June and never done.

Production, cold cache, desktop:

Connection Globe Facet panel
Unthrottled 2.2 s 10.8 s
4G 15.4 s 168.7 s
3G fast 38.7 s 423.1 s
3G slow 156.6 s never (>600 s)

On a slow connection the facet panel never appears at all.

Cross-browser (good news): works in Chromium, Firefox and WebKit/Safari, desktop and mobile viewports, zero uncaught page errors, comparable timings. The 74 MB is identical in all three, so this is not a browser quirk.

Reproducing

Both instruments are committed in perf/313-bandwidth-findings on the rdhyee fork:

  • tests/playwright/bandwidth_matrix.py β€” the measurement harness
  • tests/playwright/range_fix_proxy.py β€” the A/B proxy that isolates the cause

Full write-up with method notes and limitations: PERF_BANDWIDTH_FINDINGS_2026-08-06.md on that branch.

After the fix

index.qmd's "less than 1 MB" and explorer.qmd's "only the bytes you need" should be updated to the measured figure (~3.3 MB cold) β€” still a good story, and an honest one.


Caveat on the numbers: single runs on one machine and uplink, DevTools throttling rather than a real cellular link, cold-cache first visits only. Order-of-magnitude, not benchmarks. The A/B comparison is the reliable part, since both arms ran under identical conditions.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions