Repository navigation
P 06 Query Cancellation
Context: After P-05, the engine holds its one connection until a streamed result ends. The workbench ignored a run while another query was in progress, so a person who started a slow query and then changed their mind had two options: wait for it to finish and press Run again, or reload the page. The slow query's work was wasted either way, and its result replaced the screen before the query the person actually wanted.
Hypothesis: If a new run, a Cancel button or Esc stops the query in the engine, the connection is free at once. A query run while another is in progress then shows its rows in about the time it takes on its own, with no wasted work.
-
Engine:
streamQuery(sql, signal?)insrc/engine/engine.tstakes anAbortSignal. Aborting calls DuckDB-wasm'scancelSent(), and the stream yields no more batches. A query aborted before it reaches the connection is never sent. -
Superseded runs:
src/features/results/query-run.tsgives each run its ownAbortController. A new run aborts the one in progress instead of being ignored. An abandoned run never writes to the result store, but it still drains its stream, so the connection is always given back. This also fixed a leak: a run abandoned straight afterstreamQueryresolved never released the connection. - Cancel: a Cancel button shows while a query runs. Esc anywhere on the page also cancels, unless something like autocomplete has already used the key. The status line reads "Cancelled after X", and the result on screen is cleared, as it is after a failure.
Method:
-
Scenario:
rerun-while-running, added for this experiment (bench/scenarios/rerun.ts). It typesSELECT sum(hash(range)) AS total FROM range(150000000), a streamed query that takes about 3.5 s on the reference laptop, and runs it. While it runs, the scenario typesSELECT 42 AS answerand presses Run.rerunMsis from that press until the42row is on screen. If the press was ignored, the scenario waits until no query is running and presses Run again, as a person would.attemptscounts the presses. -
Before:
52085eb,mainjust before #117. After:6c110e9,mainwith #117 and the later grid change (#121). Both builds had the new scenario file added uncommitted, so their result files are flaggeddirty. Each was built from its own git worktree and served byvite previewwith production headers. - Interleaved, one session: the scenario ran before, after, before, after, all within about four minutes, so both builds ran through the same background load.
- Iterations: 3 warm-up and 10 measured iterations per round, two rounds per build: 20 measured iterations each. Median and p95 reported.
- Environment: the reference laptop from P-01 (Intel Core i5-8250U, 24 GB, Windows 11 Pro 10.0.26300), on mains power, with other apps open. Chromium 156, headless.
All times are in milliseconds, given as median / p95.
| Metric | Before | After | Change (median) |
|---|---|---|---|
rerunMs |
3,557 / 3,934 | 69 / 80 | −98% |
attempts |
2 / 2 | 1 / 1 | |
| Range (min to max) | 3,386 to 4,392 | 55 to 95 |
- A query run during a slow one now shows in about 70 ms instead of about 3.6 s. Before P-06, the first press was ignored in all 20 iterations. The new query only ran once the slow one finished, so the wait was the slow query's remaining time. After P-06, the first press cancels the slow query, and the new one's row shows in 55–95 ms.
-
The engine really stops.
cancelSent()interrupts the streamed query in DuckDB, and the next query gets the connection within milliseconds. Before, it waited out the slow query's 3.5 s. The end-to-end test "cancels a long query when another is run" covers a query that would otherwise run for hours. - The gain grows with the slow query. The before figure is roughly the slow query's remaining time, so a 30 s query would make a person wait up to 30 s. The after figure doesn't depend on it.
Trade-offs:
-
Only streamed queries can be stopped. Writes, several statements at once, and results with dictionary-encoded (ENUM) columns run whole through
query, as in P-05. DuckDB-wasm can't interrupt those once sent. Their results are thrown away, but the engine finishes them first, and the next query waits. - Cancelling clears the screen. A cancelled query clears the result, including rows that had already arrived, as a failure does. The previous result isn't brought back.
- The Run button stays disabled while a query runs. Double-clicks are still ignored. A new query cancels the running one when started with Ctrl/Cmd+Enter from the editor.
Limitations:
- One scenario, Chromium only. Firefox and WebKit were checked by the end-to-end tests, not benchmarked.
- The slow query is synthetic. A real slow query over a file spends its time scanning, not hashing a range, but it streams the same way.
- Background load. Run on the working laptop with other apps open. The difference is about 50×, far beyond the noise.
Evidence:
- Raw results in
bench/results:2026-10-25T09-15-03Z-52085eb.json(before) and2026-10-25T09-15-50Z-6c110e9.json(after). Each file merges a build's two rounds and is flaggedinterleaved: true. - To reproduce: run
pnpm bench --scenario rerun-while-runningat each commit, withbench/scenarios/rerun.tsand its line inbench/scenarios/index.tscopied in for the before build.
Conclusion: Adopt. A query run while another is in progress shows its rows about 98% sooner, and the engine stops the superseded work instead of finishing it. The remaining gap is queries that don't stream, which still run to the end in the engine.
⚡ Blinkql: queries in the blink of an eye · MIT licensed · Live app · Issues · Discussions
Project
Engineering
- Tech Stack
- Architecture
- Performance & Benchmarks
- Decision Records (ADRs)
- Privacy & Security
- Browser Support & Limitations
Design
Community