Two formats optimized for opposite jobs
JSON is the universal language of APIs, config, and message passing — human-readable, self-describing, row-oriented text. Apache Arrow is something different: a columnar, in-memory format built for *analytics* at scale. They're not really competitors; they're optimized for opposite workloads, and knowing where the crossover point is saves a lot of wasted CPU. By 2026 Arrow has become the "invisible standard" underneath DuckDB, Polars, pandas, Spark, and most data tooling — often without users noticing.
If you already understand binary alternatives like MessagePack and Avro, Arrow is the piece aimed specifically at columnar, in-memory analytics rather than compact transport.
Row-based text vs columnar memory
Picture a million records with the fields id, country, amount. JSON stores them row by row, and every value is text that must be parsed:
{ "id": 1, "country": "US", "amount": 42.50 }
{ "id": 2, "country": "US", "amount": 17.00 }
{ "id": 3, "country": "NO", "amount": 88.25 }Arrow stores the same data column by column, as contiguous typed arrays in memory:
id: [ 1, 2, 3, ... ] (int64, packed)
country: [ "US", "US", "NO", ... ] (dictionary-encoded)
amount: [ 42.50, 17.00, 88.25 ... ] (float64, packed)That layout difference is the whole story:
- No parse step. JSON must be tokenized and converted from text to numbers on every read. Arrow's
amountcolumn is *already* an array of float64 in memory — you compute on it directly, with zero deserialization. - Scan one column, not every row.
SELECT sum(amount)in Arrow touches only theamountbuffer. In JSON you parse every field of every record just to reach one number. - SIMD and cache friendliness. Contiguous same-type values let the CPU vectorize (process many values per instruction) and stay cache-resident. Row-based text defeats both.
- Compression. A column of repeated
countrycodes compresses far better than the same values scattered through rows, and Arrow dictionary-encodes them automatically.
The result is dramatic on analytical queries — engines report query-time reductions well past 90% versus older text/row driver paths.
Zero-copy: the other superpower
Arrow defines a *standard memory layout*, which means two processes or libraries that both speak Arrow can share a dataset without serializing it. pandas can hand a table to DuckDB, or a query engine can hand results to your Python process, by passing a pointer — no encode-to-JSON, send, decode-from-JSON round trip. Eliminating that serialization tax between tools is why Arrow spread through the whole data ecosystem.
Where each one wins
Use JSON when: - You're talking to a browser, a webhook, or a public REST API — universality and readability matter most. - Records are heterogeneous, deeply nested, or schema-flexible. - Volumes are modest and human-debuggability is worth the parse cost.
Use Arrow when: - You're doing columnar analytics — aggregations, filters, joins over millions of rows. - Data moves *between* tools (pandas ↔ DuckDB ↔ Spark) and serialization is the bottleneck. - You need to compute on data in memory without a per-value parse.
They're better together
The common production pattern isn't "pick one" — it's JSON at the edges, Arrow in the engine. Data arrives as JSON from APIs and logs (often NDJSON for streaming), gets loaded once into Arrow-backed columns, and every subsequent analytical operation runs on the columnar form. For durable storage the Arrow columns are written to Parquet (Arrow's on-disk cousin) — see JSON to Parquet. You get JSON's interoperability at the boundary and Arrow's speed everywhere inside.
For handling the JSON side at scale first, see parsing large JSON files and JSON size limits. To measure a payload before deciding, check its JSON size.