Overview
DuckDB-Wasm can be instantiated in several ways, depending on how your application bundles and serves its assets. Each approach resolves the mainModule (the WebAssembly file) and the mainWorker (the worker script) for the browser's capabilities, then hands them to AsyncDuckDB.
This page shows the patterns for a jsDelivr CDN, webpack, Vite and statically served files. Once instantiated, the db object is used to import data and run queries.
Bundle Selection
DuckDB-Wasm ships several WebAssembly modules compiled for different browser feature sets, because post-MVP WebAssembly features reach browsers at different speeds and each one can bring a flat performance improvement. The selectBundle function runs dynamic browser checks and picks the fastest bundle the current browser supports:
mvp— the WebAssembly 1.0 (MVP) baseline, supported everywhereeh— adds Wasm-level exception handling, which improves performance; DuckDB and DuckDB-Wasm are written in C++ and use exceptions to propagate errors, so native exception handling avoids emulating them through JavaScriptcoi— adds threading for parallel query execution; it requires the page to be cross-origin isolated and must be opted into explicitly (see Threading)
The examples below all call selectBundle so the browser receives the fastest bundle it can run. You can also inspect the selected bundle and feature set from the web shell with the .features command.
cdn(jsdelivr)
The simplest way to load DuckDB-Wasm is straight from a CDN, with no build step or bundler configuration. getJsDelivrBundles() returns the set of bundles hosted on jsDelivr, and selectBundle picks the one that matches the browser. Because a Web Worker script must be same-origin, the CDN worker URL is wrapped in a Blob that importScripts it, and the temporary object URL is revoked once the worker has started.
import * as duckdb from '@duckdb/duckdb-wasm';
const JSDELIVR_BUNDLES = duckdb.getJsDelivrBundles();
// Select a bundle based on browser checks
const bundle = await duckdb.selectBundle(JSDELIVR_BUNDLES);
const worker_url = URL.createObjectURL(
new Blob([`importScripts("${bundle.mainWorker}");`], {type: 'text/javascript'})
);
// Instantiate the asynchronous version of DuckDB-Wasm
const worker = new Worker(worker_url);
const logger = new duckdb.ConsoleLogger();
const db = new duckdb.AsyncDuckDB(logger, worker);
await db.instantiate(bundle.mainModule, bundle.pthreadWorker);
URL.revokeObjectURL(worker_url);
webpack
When your application is built with webpack, let the bundler resolve and emit DuckDB-Wasm's assets instead of fetching them from a CDN. Import each .wasm module directly, and reference the worker scripts with new URL(..., import.meta.url) so that webpack fingerprints them and rewrites the paths to the emitted files. The bundles are declared manually because their final locations are known only after the build, and selectBundle then chooses between the mvp and eh variants at runtime.
import * as duckdb from '@duckdb/duckdb-wasm';
import duckdb_wasm from '@duckdb/duckdb-wasm/dist/duckdb-mvp.wasm';
import duckdb_wasm_next from '@duckdb/duckdb-wasm/dist/duckdb-eh.wasm';
const MANUAL_BUNDLES: duckdb.DuckDBBundles = {
mvp: {
mainModule: duckdb_wasm,
mainWorker: new URL('@duckdb/duckdb-wasm/dist/duckdb-browser-mvp.worker.js', import.meta.url).toString(),
},
eh: {
mainModule: duckdb_wasm_next,
mainWorker: new URL('@duckdb/duckdb-wasm/dist/duckdb-browser-eh.worker.js', import.meta.url).toString(),
},
};
// Select a bundle based on browser checks
const bundle = await duckdb.selectBundle(MANUAL_BUNDLES);
// Instantiate the asynchronous version of DuckDB-Wasm
const worker = new Worker(bundle.mainWorker!);
const logger = new duckdb.ConsoleLogger();
const db = new duckdb.AsyncDuckDB(logger, worker);
await db.instantiate(bundle.mainModule, bundle.pthreadWorker);
vite
Vite resolves assets a little differently: appending the ?url suffix to an import tells Vite to return the asset's final URL rather than its contents. Import both the .wasm modules and the worker scripts this way, assemble them into the manual bundle definition, and let selectBundle select the appropriate variant for the browser at runtime.
import * as duckdb from '@duckdb/duckdb-wasm';
import duckdb_wasm from '@duckdb/duckdb-wasm/dist/duckdb-mvp.wasm?url';
import mvp_worker from '@duckdb/duckdb-wasm/dist/duckdb-browser-mvp.worker.js?url';
import duckdb_wasm_eh from '@duckdb/duckdb-wasm/dist/duckdb-eh.wasm?url';
import eh_worker from '@duckdb/duckdb-wasm/dist/duckdb-browser-eh.worker.js?url';
const MANUAL_BUNDLES: duckdb.DuckDBBundles = {
mvp: {
mainModule: duckdb_wasm,
mainWorker: mvp_worker,
},
eh: {
mainModule: duckdb_wasm_eh,
mainWorker: eh_worker,
},
};
// Select a bundle based on browser checks
const bundle = await duckdb.selectBundle(MANUAL_BUNDLES);
// Instantiate the asynchronous version of DuckDB-Wasm
const worker = new Worker(bundle.mainWorker!);
const logger = new duckdb.ConsoleLogger();
const db = new duckdb.AsyncDuckDB(logger, worker);
await db.instantiate(bundle.mainModule, bundle.pthreadWorker);
Statically Served
If you would rather not depend on a CDN or a bundler, you can host the DuckDB-Wasm files yourself. Manually download the distribution files from https://cdn.jsdelivr.net/npm/@duckdb/duckdb-wasm/dist/, serve them from your own origin, and point the bundle paths at those locations. This keeps every asset on your own server, which suits offline, air-gapped, or strict content-security-policy deployments. Update the placeholder paths below to match where you serve the files.
import * as duckdb from '@duckdb/duckdb-wasm';
const MANUAL_BUNDLES: duckdb.DuckDBBundles = {
mvp: {
mainModule: 'change/me/../duckdb-mvp.wasm',
mainWorker: 'change/me/../duckdb-browser-mvp.worker.js',
},
eh: {
mainModule: 'change/me/../duckdb-eh.wasm',
mainWorker: 'change/me/../duckdb-browser-eh.worker.js',
},
};
// Select a bundle based on browser checks
const bundle = await duckdb.selectBundle(MANUAL_BUNDLES);
// Instantiate the asynchronous version of DuckDB-Wasm
const worker = new Worker(bundle.mainWorker!);
const logger = new duckdb.ConsoleLogger();
const db = new duckdb.AsyncDuckDB(logger, worker);
await db.instantiate(bundle.mainModule, bundle.pthreadWorker);
Configuration
instantiate() starts DuckDB-Wasm with default settings. To change how the database behaves, call open() on the db object before opening a connection, passing a configuration object:
import * as duckdb from '@duckdb/duckdb-wasm';
await db.instantiate(bundle.mainModule, bundle.pthreadWorker);
await db.open({
path: ':memory:',
query: {
castBigIntToDouble: true,
},
});
const conn = await db.connect();
Commonly used fields of the configuration object:
path: the database file to open. Defaults to an in-memory database (:memory:). Use anopfs://path to persist data to the browser's Origin Private File System (see Persistence with OPFS).accessMode:duckdb.DuckDBAccessMode.READ_ONLYorduckdb.DuckDBAccessMode.READ_WRITE.allowUnsignedExtensions: allow loading extensions that are not signed (see Load Extensions).maximumThreads: the number of threads to use. This takes effect only with the threadedcoibundle on a cross-origin-isolated page (see Threading).query: controls how query results are converted from Arrow into JavaScript values.
The query object tunes the Arrow-to-JavaScript type mapping:
castBigIntToDouble: return 64-bit integers as JavaScript numbers instead ofBigIntvalues. This is convenient, but large integers can lose precision.castDecimalToDouble: returnDECIMALvalues as floating-point numbers instead of DuckDB's exact decimal representation.castTimestampToDate: returnTIMESTAMPvalues as JavaScriptDateobjects.castDurationToTime64: return duration values as 64-bit (microsecond) time values.queryPollingInterval: the interval, in milliseconds, at which streaming queries poll for results.
Threading
By default DuckDB-Wasm runs on a single thread. The threaded coi bundle runs queries across multiple threads, but it has two requirements: the page must be cross-origin isolated, and you must add the coi bundle to the set you pass to selectBundle, because getJsDelivrBundles() returns only the mvp and eh bundles. The coi bundle also needs a third artifact, the pthread worker, alongside the usual module and worker:
import * as duckdb from '@duckdb/duckdb-wasm';
const DIST = 'https://cdn.jsdelivr.net/npm/@duckdb/duckdb-wasm/dist/';
// getJsDelivrBundles() returns only mvp and eh, so add coi explicitly
const bundles: duckdb.DuckDBBundles = {
...duckdb.getJsDelivrBundles(),
coi: {
mainModule: `${DIST}duckdb-coi.wasm`,
mainWorker: `${DIST}duckdb-browser-coi.worker.js`,
pthreadWorker: `${DIST}duckdb-browser-coi.pthread.worker.js`,
},
};
// selectBundle picks coi only on a cross-origin-isolated page whose browser
// supports Wasm exceptions, SIMD, and threads; otherwise it falls back to eh or mvp
const bundle = await duckdb.selectBundle(bundles);
const worker = new Worker(bundle.mainWorker!);
const db = new duckdb.AsyncDuckDB(new duckdb.ConsoleLogger(), worker);
await db.instantiate(bundle.mainModule, bundle.pthreadWorker);
// Configure the thread count (only effective on the coi bundle)
await db.open({ maximumThreads: 4 });
const conn = await db.connect();
Because selectBundle falls back to the eh or mvp bundle when the coi requirements are not met, the same code runs everywhere: on a page that is not cross-origin isolated it simply runs single-threaded, and bundle.pthreadWorker is null. Two things to keep in mind when deploying the threaded bundle:
- The page must be served with the
Cross-Origin-Opener-Policy: same-originandCross-Origin-Embedder-Policy: require-corpheaders, which is what makes the browser exposeSharedArrayBufferand report the page as cross-origin isolated. See Cross-Origin Isolation. - Extensions must be built for the threaded platform. The
coibundle uses thewasm_threadsextension platform, so extensions published only forwasm_mvporwasm_ehwill not load into it.
Persistence with OPFS
By default a DuckDB-Wasm database lives in memory and is lost when the page closes. To persist data across page reloads and sessions, open the database from the browser's Origin Private File System (OPFS) by giving open() a path with the opfs:// scheme:
import * as duckdb from '@duckdb/duckdb-wasm';
await db.instantiate(bundle.mainModule, bundle.pthreadWorker);
await db.open({
path: 'opfs://duckdb.db',
accessMode: duckdb.DuckDBAccessMode.READ_WRITE,
});
const conn = await db.connect();
await conn.query(`CREATE TABLE t AS SELECT * FROM range(10) AS r(i)`);
// Flush changes to OPFS so they survive a reload
await conn.query(`CHECKPOINT`);
Reopening the same opfs:// path in a later session restores the tables. You can also read and write opfs:// files directly from SQL. Set opfs.fileHandling to 'auto' so that DuckDB-Wasm registers the opfs:// paths referenced in a statement automatically:
await db.open({
path: 'opfs://duckdb.db',
accessMode: duckdb.DuckDBAccessMode.READ_WRITE,
opfs: { fileHandling: 'auto' },
});
// Read a Parquet file stored in OPFS, and write a CSV back to OPFS
await conn.query(`CREATE TABLE t AS SELECT * FROM 'opfs://data.parquet'`);
await conn.query(`COPY (SELECT * FROM t) TO 'opfs://export.csv'`);
Note OPFS access relies on synchronous file access handles, which browsers expose only inside a Web Worker; DuckDB-Wasm's asynchronous API already runs in one. Keep these points in mind: call
CHECKPOINTto flush writes to disk, drop registered files withdb.dropFile()(ordb.dropFiles()) before another connection or database instance opens them, because a file can be held by only one handle at a time, and note that moving files into or out of OPFS is not yet supported.
Further Reading
- Import Data — registering files and inserting data into the instantiated database.
- Run Queries — executing queries against the
dbobject created here. - Deploy — serving the library, worker, and WebAssembly components that these bundles reference.
- DuckDB Wasm Client — the layered API and the examples the snippets above are drawn from.