sparrowflight · js
sparrowJS — Arrow Flight. In the browser. Finally. Browser-first access to Arrow Flight data via gRPC-web and Apache Arrow JS.
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
queryArrow FlightREST+JSONwinner
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).
sparrowflight.io · 2026