Tracking issue for benchmarking the MCP query/tile tasks on the DuckDB + httpfs backend — establishing where performance stands, what's rate-limiting, and pushing further. Consolidates the bench scripts on branch bench/s3-ceiling (s3-throughput / s3-ceiling / s3-diag / s3-decomp + the engine/mean-carbon probes).
TL;DR (so far)
- Real DuckDB queries are decode/compute-bound, not network- or httpfs-bound. S3 ≈ local ≈ internal-MinIO for heavy aggregates; throughput scales ~linearly with threads.
- DuckDB local/httpfs parquet reads are GB/s-class (uncompressed) and competitive with Polars / faster than PyArrow. Earlier alarming "~180–250 MB/s" numbers were measurement artifacts (see invalid probes).
- Column pruning is the dominant lever (~10×): 1-col sum ~16 GB/s vs 7-col ~1.6 GB/s uncompressed (local NVMe).
Methodology — valid metrics
- Report uncompressed GB/s and Mrow/s, never compressed-byte MB/s. Carbon hex compresses ~4–6×, so compressed-byte rates understate decode by that factor.
- Use queries that actually read+process data:
sum()/avg() over real columns, or full materialize.
- Determine rate-limiting two ways: thread scaling (scales → compute-bound; flat → I/O-bound) and local-vs-remote (equal → compute-bound).
- Set DuckDB
memory_limit explicitly: in a container DuckDB sizes it from host RAM and gets cgroup-OOMKilled at the pod limit.
Invalid probes — DO NOT use these to measure read throughput
count(*) — answered from parquet row-count metadata; reads ≈ no data.
count(COLUMNS(*)) — answered from per-column null-count metadata; reads ≈ no data. (This invalidated an earlier "DuckDB ~180 MB/s ceiling" claim — it was measuring footers, not transfer.)
read_blob as a proxy for query throughput — measures sequential whole-file transfer that does not parallelize across files (~95 MB/s over httpfs). read_parquet parallelizes ranged reads and is 13–23× faster over the same httpfs. Use read_blob only to characterize raw transport, never as the query-path number.
- Environmental confounds to control: client NIC (cirrus has a 1 Gb/s card; the MinIO external-IP hairpin capped reads at ~1 Gb/s — fixed by using the internal
minio-svc.minio.svc.cluster.local:9000), client CPU on small pods, and warm vs cold caches.
Standing results
Engine comparison, local NVMe (ZFS, warmed), carbon hex, uncompressed GB/s:
| query |
DuckDB |
Polars |
PyArrow |
| sum 1 col |
16.6 |
9.5 |
5.4 |
| read all 7 cols |
1.6 |
1.6 |
0.38 |
Representative MCP aggregates — cirrus pod (128 core), httpfs → internal MinIO, full carbon hex (4.84B rows / 13.7 GB):
| query |
T=1 |
T=8 |
T=16 |
bound by |
| mean carbon / h0 (122 grp) |
100.3s |
10.6s |
6.2s |
CPU (9.4× then 1.7× scaling) |
| mean carbon / h1 (h3 rollup) |
— |
25.1s |
14.1s |
CPU (extra col + h3 on 4.84B rows) |
Next steps
Refs: #249 (throughput bench), #103 (httpfs no conn-pool / DNS ceiling), #237 (httpfs large-chunk bug, fixed in 1.5.4), #42 / #227 (GPU path).
Tracking issue for benchmarking the MCP
query/tile tasks on the DuckDB + httpfs backend — establishing where performance stands, what's rate-limiting, and pushing further. Consolidates the bench scripts on branchbench/s3-ceiling(s3-throughput / s3-ceiling / s3-diag / s3-decomp + the engine/mean-carbon probes).TL;DR (so far)
Methodology — valid metrics
sum()/avg()over real columns, or full materialize.memory_limitexplicitly: in a container DuckDB sizes it from host RAM and gets cgroup-OOMKilled at the pod limit.Invalid probes — DO NOT use these to measure read throughput
count(*)— answered from parquet row-count metadata; reads ≈ no data.count(COLUMNS(*))— answered from per-column null-count metadata; reads ≈ no data. (This invalidated an earlier "DuckDB ~180 MB/s ceiling" claim — it was measuring footers, not transfer.)read_blobas a proxy for query throughput — measures sequential whole-file transfer that does not parallelize across files (~95 MB/s over httpfs).read_parquetparallelizes ranged reads and is 13–23× faster over the same httpfs. Useread_blobonly to characterize raw transport, never as the query-path number.minio-svc.minio.svc.cluster.local:9000), client CPU on small pods, and warm vs cold caches.Standing results
Engine comparison, local NVMe (ZFS, warmed), carbon hex, uncompressed GB/s:
Representative MCP aggregates — cirrus pod (128 core), httpfs → internal MinIO, full carbon hex (4.84B rows / 13.7 GB):
Next steps
TILE_THREADS/THREADS=48.Refs: #249 (throughput bench), #103 (httpfs no conn-pool / DNS ceiling), #237 (httpfs large-chunk bug, fixed in 1.5.4), #42 / #227 (GPU path).