Product1 publisher2 min readPublished
DuckDB-Wasm writes a real .duckdb file with a WAL into the browser's OPFS
A one-line change to the open call leaves a real database file in the browser, with a write-ahead log and checkpoints that survive a restart. Getting it to persist depends on which DuckDB-Wasm build you installed.
The Product Desk · Product desk

What happened
- DuckDB says a database opened at an opfs:// path in DuckDB-Wasm becomes a regular .duckdb file with a write-ahead log and checkpoints, and survives page reloads and browser restarts.
- Until now, persistence was application code: serialize tables to Parquet, store the bytes in IndexedDB, and re-register them on every page load, none of it offered by the library.
- OPFS, a per-origin sandboxed file system with random-access reads and writes, has shipped in modern browsers since March 2023, and DuckDB-Wasm can now use it as a storage backend.
- DuckDB tells developers to pin 1.32.0 or install the next tag, 1.33.1-dev64.0 or later, to get a build that writes to OPFS.
Compiled by The Product DeskSomething wrong?How this is made
Why it matters
- decision Teams carrying a hand-rolled Parquet-to-IndexedDB layer now have to decide whether to delete it, and keeping it means maintaining two storage paths for the same rows.
- exposure Anyone who installs without specifying a tag ships a store that loses every row when the session ends, and the application code has no reason to complain.
- capability Because the file is a standard DuckDB database, a user can hand support the exported file and an engineer can open it in the CLI or Python client to see the actual rows.
- constraint Persistence is now tied to a version pin, so a routine dependency bump can quietly turn a working browser database into an empty one.
Reload the page, run the same insert, and the count prints 2 [7]. The row that survived is in a regular .duckdb file with a write-ahead log and checkpoints, sitting in the browser's Origin Private File System [1][6]. DuckDB's walkthrough gets there without an export step or a localStorage key to remember [7].
Since 2021, keeping a table across a page load meant serializing it to Parquet, pushing the bytes into IndexedDB, and re-registering them on the next load, all of it code the application team wrote and maintained [2][3]. There was no browser primitive underneath it until OPFS shipped in March 2023, about two years after DuckDB-Wasm launched [4][2]. The library change is one call: open the database at an opfs:// path instead of the default in-memory one [16]. The rest of the setup is unchanged, including wrapping the CDN worker URL in a Blob because worker scripts have to be same-origin [17].
The version story is where a Monday rollout goes wrong. The build npm serves as latest, 1.33.1-dev57.0, creates the OPFS files and never writes to them, so nothing persists [9]. It canonicalizes the path to opfs:/analytics.duckdb with a single slash, which no longer matches the OPFS handle [10]. The build DuckDB says works is 1.33.1-dev64.0, seven dev builds later [11][1].
A smoke test that checks whether the file exists in OPFS passes on the broken build, because the file does exist and is empty [3]. The check that separates the two is a row count taken after a full browser restart, not after a reload inside the same session [1].
The same opfs:// prefix applies to data files, which is what makes the fetch-once pattern work: load a remote dataset into the persistent database and cache derived results as Parquet alongside it [13]. DuckDB-Wasm pulls the remote file with HTTP range requests, and because the table is created with IF NOT EXISTS, the fetch happens only on the first page load [15]. The worked example is small, the TPC-H orders table at scale factor 0.01, roughly 1,500 rows from the DuckDB web shell [14].
For a team weighing whether to delete its IndexedDB shim, two facts decide it. The first is whether the data has to survive a browser restart or only a route change in the same tab; a session-scoped cache gains nothing here. The second is whether you can hold a dependency version in production, because the default install writes nothing to disk [9][11]. Teams that clear both swap hand-written serialization for a pinned version and a CHECKPOINT after writes [18].
What to watch
- Whether the npm latest tag picks up the path fix, so a plain install persists without a pin.
- Whether app teams delete their Parquet-to-IndexedDB code or keep it as a fallback for older browsers.
- Whether DuckDB publishes OPFS write throughput or size limits beyond the 1,500-row demo table.