Performance & Memory
How DuckViz manages browser memory, when it warns, and how to tune limits for your hardware.
DuckViz runs entirely in the browser tab — DuckDB-WASM, Web Workers, and React all share the JS heap. This guide explains the safeguards that keep the tab responsive and how to tune them.
Where the freezes come from
A browser tab freezes long before it actually crashes. The two failure modes:
- Memory pressure. When the JS heap gets close to its hard ceiling (
performance.memory.jsHeapSizeLimit), the browser starts throwingRangeError: Out of memoryfrom any allocation — including the React reconciler. The UI looks frozen because every paint is failing. - Main-thread starvation. A long-running synchronous task (parsing a 200 MB CSV, rendering 50 000 SVG circles, JSON-stringifying a multi-megabyte object before posting it to a worker) blocks the event loop. The tab is alive, just unresponsive.
DuckViz mitigates both with hard caps, predictive warnings, and worker offload.
Memory budgets
The memory monitor polls JS heap + IDB + DuckDB internals every 10 seconds and adapts its threshold to the device.
| Tier | Default ceiling | Detection |
|---|---|---|
| Desktop Chromium | 1024 MB | performance.memory.jsHeapSizeLimit > 2 GB |
| Mobile Safari/iOS | 256 MB | navigator.userAgent matches iOS/iPadOS |
| Other / unknown | 512 MB | Fallback. Most Android Chrome falls here. |
The user can override the ceiling from Settings → Performance. The override is stored in localStorage and survives reloads, but is not synced to the server (per-device).
Hysteresis
When usage crosses 100% of the ceiling, a non-closable CriticalMemoryModal blocks further ingest until usage falls below 90%. This avoids flapping when a single GC cycle dips just under the threshold.
Predictive warning
The monitor projects the next 30 seconds of growth from a linear regression on the last six samples. If the projection crosses the ceiling, the warning appears early — before the UI starts to stutter.
Pre-flight file warnings
Drop a 500 MB CSV onto the home page and you will see a confirm modal listing the oversized files before any of them touch DuckDB. The threshold is largeFileWarnMB (default 0 = disabled when embedding <Explorer>; the app sets it from the user's perf settings).
The same modal is wired into:
apps/app/app/home/home-quick-actions.tsx— drag/drop and file pickers<ExplorerSidebar>— the+add-files affordance inside/directory
If the user picks Skip large files, the offending entries are dropped before ingest() is called; Add anyway continues with the original list.
Ingest size matrix
These numbers are observed end-to-end (file → DuckDB table → first chart) on a 2024 MacBook Air M3 (Chrome 131) and an iPhone 14 Pro (Safari iOS 18). Anything above the freeze risk column will reliably trigger the memory modal or stall the tab.
| File type | Smooth | Slow but works | Freeze risk | Notes |
|---|---|---|---|---|
| CSV | ≤ 50 MB | 50 – 200 MB | ≥ 250 MB | Arrow ingest path; column types inferred client-side. |
| Parquet | ≤ 100 MB | 100 – 400 MB | ≥ 500 MB | Cheapest format — already columnar. |
| JSON / NDJSON | ≤ 25 MB | 25 – 100 MB | ≥ 150 MB | Schema inference walks every row. |
| XLSX | ≤ 10 MB | 10 – 40 MB | ≥ 60 MB | XLSX → CSV conversion happens in a worker; the conversion itself doubles memory. |
| Log files | ≤ 30 MB | 30 – 120 MB | ≥ 200 MB | Rust-WASM parser + LLM format detection sample. |
| Mobile Safari | divide by 4 — keep individual files under ~25 MB. |
The matrix assumes the file is the only thing in the tab. Two 100 MB CSVs will not behave like one 200 MB CSV — DuckDB keeps each table fully resident.
Stall detection
The ingest worker waits for batch acknowledgements from the main thread. If an ack does not arrive within 15 s, the worker posts an ingest-stalled progress event with the path, table, batch index, and waited-ms. The main UI logs a warning so a stuck ingest does not look like silent failure.
Auto-LIMIT for ad-hoc queries
<DuckvizDBProvider> accepts a defaultRowCap prop (shipped in @duckviz/db@0.6.0). When set, every runQuery(sql) call is rewritten to append LIMIT <cap> — unless the SQL already ends in LIMIT N (or LIMIT N OFFSET N). This is the safety net for LLM-generated SQL: even if the model emits an unbounded SELECT * FROM huge_table, the query returns at most cap rows.
The cap is conservative — it only checks for a trailing LIMIT, so nested CTE limits still get capped at the outer level. Non-SELECT statements (SHOW, DESCRIBE, PRAGMA, INSERT, CREATE) are skipped automatically (0.6.1). Set defaultRowCap={0} (the default) to disable.
In apps/app, the cap comes from usePerfSettingsStore:
const { autoLimit, rowCapHard } = usePerfSettingsStore();
const defaultRowCap = autoLimit ? rowCapHard : 0;Chart-side row caps
D3 SVG charts cannot render arbitrary row counts — at some point the browser refuses to paint that many DOM nodes. Each chart has a hard cap and shows a top-right "Showing N of M" badge when the cap kicks in.
| Chart type | Cap | Why |
|---|---|---|
| Table widget | 10 | Widget context — not a paginated grid. |
| Bubble | 500 | Each bubble is an SVG circle with hover handlers. |
| Circle packing | 500 | Hierarchy layout cost grows super-linearly. |
| Parallel coordinates | 1 000 | One polyline per row. |
| Heatmap | 2 000 | Cell count = row × col cells. |
| Contour | 5 000 | Density estimation cost dominates. |
The caps are enforced via applyChartCap() from @duckviz/widgets — a tiny helper that returns { rows, total, capped } so each chart can also call drawRowCapBadge() to surface the truncation.
Chart render errors
If a chart still throws — bad data, unexpected null, library bug — <ChartErrorBoundary> from @duckviz/widgets catches it and renders a small inline fallback instead of crashing the whole dashboard. The boundary's resetKey is ${duckdbQuery}|${type}|${rowCount}, so editing the SQL or swapping viz type retries automatically.
Tuning checklist for hosts
When you embed <Explorer>, <Dashboard>, or <Report> in your own app:
- Set
<DuckvizDBProvider defaultRowCap={5000} />— pick a number you can render. - Pass
<Explorer largeFileWarnMB={250} />so users get warned before dropping huge files. - Listen for
ingest-stalledif you have your own progress UI; otherwise the default console warn is enough. - Wrap user-supplied chart configs in
<ChartErrorBoundary>if you render charts outside<Dashboard>(the dashboard wraps internally).