Local-first analytics in practice: DuckDB, Parquet, and killing the round-trip
By Arshad Ansari
In the previous post I argued that a lot of analytics infrastructure exists to solve laptop-sized problems. Here's what the alternative actually looks like in code.
The setup: data as files, not endpoints
The local-first pattern starts by treating your dataset as a file — ideally Parquet, a columnar format that's compact, typed, and splittable. You can sit it in object storage, ship it in a release, or cache it on disk. No connection string, no credentials rotation, no warehouse to keep warm.
Then you point an in-process engine at it. DuckDB reads Parquet natively and runs the query where you are.
import duckdb
# No server. No connection pool. Just a query against a file.
con = duckdb.connect()
result = con.execute("""
SELECT
symbol,
date_trunc('month', trade_date) AS month,
avg(close) AS avg_close,
count(*) AS days
FROM 'prices/*.parquet' -- glob straight over partitioned files
WHERE trade_date >= '2024-01-01'
GROUP BY 1, 2
ORDER BY 1, 2
""").df() # straight into a pandas DataFrame
print(result.head())
That query reads only the columns it needs (columnar), only the row groups that match the predicate (pushdown), and never leaves the machine. On a few gigabytes of price data it returns in milliseconds — on the same laptop you're reading this on.
It runs in the browser too
The part that surprises people: the same engine compiles to WebAssembly. With duckdb-wasm you can ship a Parquet file and a query to the browser and render an interactive analytics view with no backend at all.
import * as duckdb from '@duckdb/duckdb-wasm'
const db = await initDuckDB() // wasm engine in the tab
const con = await db.connect()
await con.query(`
SELECT regime, count(*) AS n
FROM 'https://cdn.example.com/regimes.parquet'
GROUP BY regime
`)
The user's machine does the work. Your "API" is a static file on a CDN. Your hosting bill is whatever a CDN charges to serve a few megabytes — and there's no query endpoint to attack, rate-limit, or scale.
Where this wins
This pattern quietly replaces a surprising amount of infrastructure:
- Internal dashboards over data that updates daily, not per-second.
- Embedded analytics in a product, without standing up a query service.
- Data exploration where analysts want SQL speed without warehouse round-trips.
- Reproducible reports — the data is the artifact; pin the Parquet, pin the result.
The honest boundaries
You give up live concurrent writes, centralized governance, and real-time freshness. If you need those, keep your warehouse. But for the broad middle — read-mostly, modest-sized, latency-sensitive analytics — local-first is faster to build, cheaper to run, and easier to reason about.
This is one chapter's worth of an idea; Local-First Analytics works through the full architecture, including caching, freshness, and when to graduate back to a warehouse. And if you're staring at a data stack that's heavier than it needs to be, that's exactly the kind of problem I help with.
Common questions
- Can DuckDB query Parquet files directly, without loading them first?
- Yes — you can put a file path or a glob straight in the FROM clause, as in SELECT ... FROM 'prices/*.parquet'. There is no import step and no schema to declare. Because Parquet is columnar, DuckDB reads only the columns the query names, and predicate pushdown means it skips row groups that cannot match the WHERE clause. On a few gigabytes that is a millisecond-scale query on an ordinary laptop.
- Can DuckDB run in the browser?
- Yes. The same engine compiles to WebAssembly as duckdb-wasm, so you can ship a Parquet file to a CDN and run SQL against it inside the tab, with no backend at all. The user's machine does the work, your API is a static file, and there is no query endpoint to scale, rate-limit or attack. It suits embedded analytics and interactive reports over data that updates daily rather than per-second.
- Do I need a data warehouse to run analytics on Parquet?
- Not for the broad middle: read-mostly, modest-sized, latency-sensitive analytics. Internal dashboards over daily data, embedded product analytics, analyst exploration and reproducible reports all work with files in object storage and an in-process engine — no connection string, no credentials to rotate, no warehouse to keep warm and no per-query meter.
- When does local-first analytics stop working?
- When you need live concurrent writes, centralised governance with roles and row-level security, or real-time freshness. Those are warehouse features and this pattern does not have them. The useful part is that the exit is cheap: if Parquet on object storage is your source of truth, graduating means pointing a different engine at data that is already in an open format, in place.
Does DuckDB fit your system?
The 16-question production-fit checklist I run before putting DuckDB on a critical path — writers, working set, durability, memory, and who talks to it. Each question comes with what a bad answer sounds like.
One email with the whole checklist. Nothing follows it. Reply and it reaches me, not a queue.
Want the whole playbook?
If this was useful, the long version is my book. Local-First Analytics — 314 pages, runnable code for every chapter — is the full build: DuckDB, Parquet and Arrow, from install to production. On Amazon, or request a free review copy.
Get the bookNot ready to buy? Read chapter 1 free — the whole chapter, no email required.
Rather talk it through? Book a free 30-minute call. No slot that suits your time zone? Email [email protected].