DuckVizBeta
Guides

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:

  1. Memory pressure. When the JS heap gets close to its hard ceiling (performance.memory.jsHeapSizeLimit), the browser starts throwing RangeError: Out of memory from any allocation — including the React reconciler. The UI looks frozen because every paint is failing.
  2. 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.

TierDefault ceilingDetection
Desktop Chromium1024 MBperformance.memory.jsHeapSizeLimit > 2 GB
Mobile Safari/iOS256 MBnavigator.userAgent matches iOS/iPadOS
Other / unknown512 MBFallback. 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 typeSmoothSlow but worksFreeze riskNotes
CSV≤ 50 MB50 – 200 MB≥ 250 MBArrow ingest path; column types inferred client-side.
Parquet≤ 100 MB100 – 400 MB≥ 500 MBCheapest format — already columnar.
JSON / NDJSON≤ 25 MB25 – 100 MB≥ 150 MBSchema inference walks every row.
XLSX≤ 10 MB10 – 40 MB≥ 60 MBXLSX → CSV conversion happens in a worker; the conversion itself doubles memory.
Log files≤ 30 MB30 – 120 MB≥ 200 MBRust-WASM parser + LLM format detection sample.
Mobile Safaridivide 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 typeCapWhy
Table widget10Widget context — not a paginated grid.
Bubble500Each bubble is an SVG circle with hover handlers.
Circle packing500Hierarchy layout cost grows super-linearly.
Parallel coordinates1 000One polyline per row.
Heatmap2 000Cell count = row × col cells.
Contour5 000Density 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:

  1. Set <DuckvizDBProvider defaultRowCap={5000} /> — pick a number you can render.
  2. Pass <Explorer largeFileWarnMB={250} /> so users get warned before dropping huge files.
  3. Listen for ingest-stalled if you have your own progress UI; otherwise the default console warn is enough.
  4. Wrap user-supplied chart configs in <ChartErrorBoundary> if you render charts outside <Dashboard> (the dashboard wraps internally).