v0.4.0 —
npm install @sparrowflight/js ·
it powers the
demo on this site · open source on
GitHub
Arrow Flight was designed for
high-performance analytics.
Browsers were left behind.
Browsers never learned gRPC, so most Arrow Flight deployments flatten columnar
data into JSON before a chart can touch it.
sparrowJS closes that gap — the missing
browser client for Apache Arrow Flight: Flight over
gRPC-web, with Apache Arrow's JavaScript
library decoding streaming record batches directly into typed arrays.
No JSON, no REST gateway, no copies.
sparrowJS exists because every path from a Flight server to a browser used to end
the same way: an expensive REST gateway turning Arrow back into JSON.
JavaScript Flight clients already exist — for Node.js, riding native gRPC that
browsers cannot speak. Browser dashboards still rely on REST+JSON gateways.
sparrowJS is a browser implementation of Flight over
gRPC-web, validated against four independent Flight servers.
This isn't a mock-up.
The demo on this site
is sparrowJS running in your browser, querying a
136-million-row
production server. Forty years of WTI stream directly from Arrow Flight and
render in the browser in about half a second. The timings are on screen.
We even built the control group: the same lake behind a typical
REST+JSON backend (DuckDB → array-of-objects →
gzip, same snapshot, same nginx), raceable button-against-button on
the demo. The result isn't a
landslide. It's a crossover — and that's exactly what we'd expect.
the race, measured july 2026 — timings vary with your link; run it yourself
| query | Arrow Flight | REST+JSON | winner |
| 1 series · 10,217 rows |
345 ms · 201 KB (2-RTT SQL) |
240 ms · 56 KB gz |
REST — one round trip beats Flight's two |
1 series · same, via pull() (1-RTT ticket) |
143 ms · 201 KB |
149 ms · 56 KB gz |
dead heat — the ticket path removes Flight's extra round trip (measured 2026-07-17, warm medians) |
1 series · same, plain query() (auto-routed 1-RTT, v0.4.0) |
137 ms · 201 KB |
149 ms · 56 KB gz |
the server advertises a sql ticket template; arbitrary SQL rides one round trip — no code change |
| 10 series · 71,979 rows |
588 ms · 1.7 MB |
850 ms · 4.8 MB JSON |
Arrow — the backend's JSON factory becomes the bottleneck |
Why it flips: for a small query, latency dominates — REST
pays one HTTP round trip; Flight SQL pays two (GetFlightInfo, then DoGet) — and
gzip'd JSON is compact while JSON.parse of 10k
rows costs almost nothing. The second row shows the counter:
pull() sends a client-constructed ticket
straight to DoGet — on servers that accept them (this one does), Flight pays
one round trip too, and the "REST wins small pulls" rule dissolves. The third
row makes it the default: the server advertises its ticket
contract in-band (a GetSqlInfo vendor code), and since v0.4.0
query() routes any statement through it
automatically — the 2-RTT row survives only as the portable baseline. As the data grows, the economics reverse: the backend
must manufacture JSON — serialize, compress, and the browser
re-materializes it as objects — a tax that scales with every row, while Arrow
record batches stream through untranslated into typed arrays. The crossover sits
around one chart's worth of data; everything a real desk pulls is past it.
But 1.4× on one polite query was never the argument. Here's where the difference
actually compounds:
Server cost
The hidden half of that 850 ms — Arrow forwards columnar buffers already in memory; REST must manufacture JSON for every request. Measured on the serving box, 16 concurrent clients pulling the ten-series batch: Arrow 13.8 req/s, p95 1.4 s · REST 3.1 req/s, p95 8.4 s. The JSON factory saturates the server at three requests a second; Flight serves 4.5× the load at 6× better tail latency.
Types
The schema rides the wire — JSON has no int64, no decimal, no timestamp, no NaN-vs-null; every REST API grows an unwritten schema that every client re-implements. An Arrow price arrives as the same eight bytes it left as.
Scale
72k rows is polite — at a million rows, JSON is ~65 MB of text parsed into a million objects (GC pauses, tabs dying); Arrow is ~20 MB into typed arrays. Few systems deliberately ship a million rows as JSON. Flight does it routinely.
Streaming
First paint vs the spinner — REST is atomic: nothing exists until the last gzip byte parses. Flight renders as batches arrive. Measured on the demo's
"watch it stream" button: the first chart paints at
~270 ms — before the entire REST response has even landed (606 ms) — and all ten charts complete by ~900 ms across 36 record batches.
Compute
Arrow arrives compute-ready, not display-ready — the demo does min/max/mean directly on the arriving Float64Array. JSON must be re-materialized before any math — in the browser, in Python, in Excel. Arrow is the same buffer everywhere.
Built once
The whole point — the REST side is two hardcoded endpoints for one client type, a backend to write, deploy and extend forever. The Flight side takes any SQL. One endpoint serves browsers, Excel, Python, BI tools and AI agents.
So the honest pitch, straight from our own benchmark: for one small chart, JSON is
fine — we show you so ourselves. It's everything after the first chart where Arrow
compounds: server cost, scale, types, streaming, and never building a backend again.
Transport
gRPC-web — Flight GetFlightInfo and DoGet over the protocol browsers can actually speak, with TLS and bearer tokens just like a native client
Data
Apache Arrow JavaScript — record batches decoded directly into typed arrays, ready for charts, grids and analytics
Streaming
Incremental rendering — charts begin drawing while record batches are still arriving
Dashboards
Built for the web — the same Flight server that feeds Excel, AI agents and the CLI now feeds browser dashboards too
The edge
Proven in production — gRPC-web needs a translation layer. The one powering this site runs live today: nginx → Envoy grpc_web → Flight server
the api, as shipped — npm install @sparrowflight/js
import { FlightClient } from "@sparrowflight/js";
const client = await FlightClient.connect({
endpoint: "/flight",
user: "demo", // Basic in, Bearer adopted automatically
});
// Flight SQL — the everyday path (auto-routes to 1 RTT on Sparrow nodes)
const stream = client.query(`
SELECT period, value FROM series_data
WHERE series_id = 'PET.RWTC.D'
`);
for await (const batch of stream) {
chart.append(batch); // Apache Arrow JS RecordBatch
}
// the same series as a Direct Pull — a ready ticket, one round trip
for await (const batch of client.pull(["PET.RWTC.D"], { start: "2020-01-01" }))
chart.append(batch);
// raw Flight, any server: client.getFlightInfo(desc) → client.doGet(ticket)
Published on npm, running in production on this site.
Everything above ships today:
connect() with
fail-fast auth and automatic Bearer adoption, a
QueryStream you can async-iterate
or await,
pull() — the Direct
Pull, one round trip for a known series, same verb as the CLI's
sparrow pull — wire stats on every
result, a typed query builder,
capabilities() /
tables() metadata, and an
int64 policy that never silently loses
precision. 53 tests run against three public Flight SQL dialects on every
push — and
the live demo
streams the 136-million-row production instance through this exact code.
It also decodes what Arrow JS can't yet. Modern
DataFusion servers ship strings as
Utf8View,
a type no released Arrow JS can read — sparrowJS transcodes View types to
classic Arrow at the IPC layer, client-side, so those servers work in the
browser as-is. Details in
the Flight reference.
The code is public —
Apache-2.0,
on GitHub — and on npm:
npm install @sparrowflight/js (44 kB gzipped, ESM, TypeScript types;
apache-arrow as peer).