One hundred weekly petroleum series — ~140,000 rows —
in a single Arrow Flight SQL call. Stream it (charts draw as batches
arrive) or draw at the end. The third button is the fairest fight we can
offer JSON: one REST call, gzipped, same server,
same lake — the best the conventional way gets. The numbers are measured
in your browser, nothing faked.
GizmoSQL C++Sparrow Pythonengine
the page loads no data until you click — the call happens in your browser
the scoreboard — same 140,709 rows every row · measured 2026-07-18 · the buttons above rerun it in your browser
approach
first chart
all 100
on the wire
the story
Arrow Flight — Sparrow, 1-RTT + lz4
0.16 s
0.27 s
1.0 MB Arrow (lz4)
fastest of all, both metrics — the ticket skips a round trip and the stream ships compressed
Arrow Flight — stream (GizmoSQL, 2-RTT)
0.23 s
0.39 s
3.8 MB Arrow
C++ engine, standard 2-RTT, uncompressed — still quick, but a round trip and 3.8× the wire behind
REST + JSON — one gzipped call
~0.6 s
0.90 s
0.8 MB gz → 9.9 MB JSON
now the bigger wire once Arrow is compressed — and half its time is one main-thread JSON.parse stall; under load the factory caps at 3.1 req/s vs Arrow's 13.8
ROAPI — Utf8View (benched)
21 s
81 s
290.3 MB
uncompacted View buffers: 76× the wire for the same data
What the 16-user button shows, explained. Run it and
watch the medians: JSON's median user is often served faster
than Arrow's — and its slowest user waits far longer. That's the
signature of a serializing factory. A JSON API spends real CPU
manufacturing each response, so concurrent requests form a queue: the
first customers are served quickly, and every later user inherits the
whole queue in front of them. Medians look fine; tails grow with every
added user. Arrow's columnar serialization is cheap enough that the same
box runs all sixteen nearly in parallel — everyone waits about the same,
and the slowest user is barely worse than the median. Nobody's dashboard
is judged by its median load; it's judged by the person staring at a
spinner. That's why the number to read is the slowest
user — and why the gap between the two columns widens with every
user you add.
JSON used to have two things going for it here: one round
trip and a small wire. Sparrow now has both. Flight SQL reads are two
RPCs by design — submit the query, get a ticket, then stream it —
because the ticket can point at several endpoints on several nodes: that
indirection is what lets a big result scatter across a cluster. REST carries
no such concept, so it wins the tiny pulls against the standard flow.
But this server accepts client-constructed tickets, so
query() skips the ask-step and streams in
one round trip — and it now ships that stream
lz4-compressed (sparrowJS decodes it in the browser
via arrow-js 21.2 + lz4js).
The result: the 100-chart pull dropped from ~0.93 s to 0.27 s and its
wire from 3.8 MB to 1.0 MB — now in the same ballpark as
JSON's 0.8 MB gzip, but with no
JSON.parse stall on the other end (Arrow
hands typed arrays, not two million object properties). One round trip and a
small wire were JSON's last two advantages here; compression takes the wire.
ROAPI (the third engine on the 10-chart demo) sits this
one out: DataFusion currently serializes this query's Utf8View string columns
with uncompacted shared buffers — ~290 MB on the wire for these
3.8 MB of data, measured. The charts decode correctly (sparrowJS transcodes
View types client-side), but a 76× wire bill is a stall by any name.
Details in the Flight reference.