Minimal DuckDB WASM for Cloudflare Workers. Async API with full TypeScript support.
Important: This package requires a Cloudflare Workers Paid Plan due to the WASM size (~9.7MB). The free plan has a 3MB limit, while paid plans support up to 10MB.
npm install @ducklings/workers
import { init, DuckDB } from '@ducklings/workers';
import wasmModule from '@ducklings/workers/wasm';
export default {
async fetch(request: Request): Promise<Response> {
// Initialize with pre-compiled WASM module
await init({ wasmModule });
const db = new DuckDB();
const conn = db.connect();
// All queries are async
const rows = await conn.query<{answer: number}>('SELECT 42 as answer');
conn.close();
db.close();
return Response.json(rows);
}
};
For production use, reuse the database and connection across requests to avoid re-initialization overhead:
import { init, DuckDB, type Connection } from '@ducklings/workers';
import wasmModule from '@ducklings/workers/wasm';
// Global state (reused across requests in the same Worker instance)
let db: DuckDB | null = null;
let conn: Connection | null = null;
let initialized = false;
async function ensureInitialized(): Promise<void> {
if (initialized && db && conn) {
return;
}
await init({ wasmModule });
db = new DuckDB();
conn = db.connect();
initialized = true;
}
export default {
async fetch(request: Request): Promise<Response> {
await ensureInitialized();
const rows = await conn!.query('SELECT 42 as answer');
return Response.json(rows);
}
};
For projects using Vite with @cloudflare/vite-plugin to build Cloudflare Workers, we provide a plugin that handles WASM file resolution and copying:
// vite.config.ts
import { defineConfig } from 'vite';
import { cloudflare } from '@cloudflare/vite-plugin';
import { ducklingsWorkerPlugin } from '@ducklings/workers/vite-plugin';
export default defineConfig({
plugins: [
ducklingsWorkerPlugin(),
cloudflare(),
],
});
ducklingsWorkerPlugin({
// Name of the WASM file in the output directory (default: 'duckdb-workers.wasm')
wasmFileName: 'duckdb-workers.wasm',
})
The plugin:
@ducklings/workers/wasm imports to a relative path for wranglerfetch()Cloudflare Workers doesn't support synchronous XMLHttpRequest (a browser-only API). This package uses Emscripten's Asyncify to enable async fetch() calls, making httpfs work properly for loading remote Parquet, CSV, and JSON files.
| Package | API Style | Size (gzipped) | httpfs |
|---|---|---|---|
@ducklings/browser |
Async (Web Worker) | ~5.7 MB | XMLHttpRequest |
@ducklings/workers |
Async (Asyncify) | ~9.7 MB | fetch() via Asyncify |
import { init, version } from '@ducklings/workers';
import wasmModule from '@ducklings/workers/wasm';
// Initialize with pre-compiled WASM
await init({ wasmModule });
// Get DuckDB version
console.log(version()); // "v1.4.3"
const conn = db.connect();
// Returns array of objects
const rows = await conn.query<{id: number, name: string}>('SELECT * FROM users');
// Returns Arrow Table (Flechette)
const table = await conn.queryArrow('SELECT * FROM users');
// Execute without returning results
await conn.execute('INSERT INTO users VALUES (1, "Alice")');
// Insert Arrow IPC data into a table
const ipc = tableToIPC(arrowTable, { format: 'stream' });
await conn.insertArrowFromIPCStream('my_table', ipc);
const stmt = conn.prepare('SELECT * FROM users WHERE id = ? AND active = ?');
stmt.bindInt32(1, 42);
stmt.bindBoolean(2, true);
const results = await stmt.run(); // Note: async
stmt.close();
// Query remote Parquet file
const rows = await conn.query(`
SELECT * FROM 'https://example.com/data.parquet'
LIMIT 10
`);
// Query remote CSV
const csv = await conn.query(`
SELECT * FROM read_csv('https://example.com/data.csv')
`);
// Query remote JSON
const json = await conn.query(`
SELECT * FROM read_json('https://example.com/data.json')
`);
Access private S3-compatible storage using DuckDB secrets:
// Cloudflare R2
await conn.execute(`
CREATE SECRET my_r2 (
TYPE R2,
KEY_ID 'your-r2-access-key-id',
SECRET 'your-r2-secret-access-key',
ACCOUNT_ID 'your-cloudflare-account-id'
)
`);
const r2Data = await conn.query(`SELECT * FROM 'r2://bucket/file.parquet'`);
Supported secret types: S3, R2, GCS
For Cloudflare Workers, you can use Workers Secrets to securely store credentials. See the example worker for a complete implementation.
Return query results as Arrow IPC stream for efficient data transfer:
import { init, DuckDB, tableToIPC } from '@ducklings/workers';
import wasmModule from '@ducklings/workers/wasm';
export default {
async fetch(request: Request): Promise<Response> {
await init({ wasmModule });
const db = new DuckDB();
const conn = db.connect();
const table = await conn.queryArrow('SELECT * FROM range(1000) t(i)');
const ipcBytes = tableToIPC(table, { format: 'stream' });
conn.close();
db.close();
return new Response(ipcBytes, {
headers: { 'Content-Type': 'application/vnd.apache.arrow.stream' },
});
}
};
// Manual control
await conn.beginTransaction();
try {
await conn.query('INSERT INTO accounts VALUES (1, 1000)');
await conn.commit();
} catch (e) {
await conn.rollback();
throw e;
}
// Or use the wrapper (auto-rollback on error)
await conn.transaction(async () => {
await conn.query('INSERT INTO orders VALUES (...)');
return 'success';
});
import { tableFromArrays, tableFromIPC, tableToIPC, utf8 } from '@ducklings/workers';
// Query as Arrow Table
const table = await conn.queryArrow('SELECT * FROM users');
// Build Arrow tables
const custom = tableFromArrays({
id: [1, 2, 3],
name: ['Alice', 'Bob', 'Charlie']
});
// Serialize/deserialize Arrow IPC
const bytes = tableToIPC(table, { format: 'stream' });
const restored = tableFromIPC(bytes);
// Insert Arrow IPC data directly into a table
const data = tableFromArrays(
{ id: [1, 2], label: ['x', 'y'] },
{ types: { label: utf8() } } // Use plain utf8 (see note below)
);
const ipc = tableToIPC(data, { format: 'stream' });
await conn.insertArrowFromIPCStream('my_table', ipc);
Dictionary encoding: Flechette's
tableFromArrays()defaults todictionary(utf8())for string columns. The Arrow IPC decoder used internally does not support dictionary-encoded streams. When building tables forinsertArrowFromIPCStream(), explicitly set string columns toutf8():import { utf8 } from '@ducklings/workers';
tableFromArrays({ col: ['a', 'b'] }, { types: { col: utf8() } });
For browser environments, use @ducklings/browser instead, which has a smaller WASM size (~5.7MB) and runs queries in a Web Worker.
INSTALL/LOAD commands for other extensions will not work.MIT
Ducklings Workers - Minimal DuckDB for Cloudflare Workers
This package provides a lightweight DuckDB binding for WebAssembly, designed for Cloudflare Workers.
IMPORTANT: In this build, query() and execute() are async and return Promises. Always use:
await conn.query(...)orawait conn.execute(...)