DuckDB 2.0, codenamed Cyanoptera, went into feature freeze on 2 September 2026, and the team is projecting the release for the second half of October (DuckDB). The alpha is out now, and the first benchmarks against 1.5.5 are large: up to 3× on remote Parquet, roughly 6× for semi-structured data stored as VARIANT instead of JSON text, and recursive queries that went from seconds to a tenth of a second (MotherDuck).
Numbers like that only help you if you know which of your queries they apply to. This article explains where each speed-up comes from, so you can tell whether your workload will see it, shows the one benchmark where 2.0 is slower, and lists what to check before you point production at it.
Install the alpha next to your current version
The alpha installs into its own place, so it does not replace the DuckDB your application uses. The CLI and the Python client are available now; other clients, including Node, will move to 2.0 gradually (DuckDB).
$ curl https://install.duckdb.org | DUCKDB_VERSION=alpha bash$ ~/.duckdb/cli/latest/duckdb -c "SELECT version() AS version;"$ pip install duckdb --pre --upgrade$ python3 -c "import duckdb; print(duckdb.version())"If your service talks to DuckDB from Node, test with the CLI against the same files the service reads. The engine is what changed; the client binding is a thin layer over it.
Remote reads overlap the network and the CPU
In 1.5.5, each worker thread alternates between downloading a chunk of a file and decoding it. While it waits on the network, its CPU core sits idle, and you can never have more downloads in flight than threads. In 2.0, a separate download pool fetches row groups ahead of time and buffers them, and the worker threads only decode (MotherDuck). The asynchronous I/O covers Parquet and CSV reads, DuckDB's own file format, and Parquet writes (DuckDB).
That explains the shape of the results. Large files with many row groups gain the most, because there is plenty to read ahead. Thirty tiny files barely change, because the time goes to per-file round trips (the footer first, then the data), which reading ahead cannot remove.
Source: MotherDuck, Why DuckDB 2.0 is faster (M5 laptop, S3 us-east-1)
| Speed-up | |
|---|---|
| 23 Parquet files, 13.6 GB | 3× |
| One 2.2 GB Parquet file | 2.4× |
| One 1.7 GB CSV file | 2.1× |
| 30 tiny Parquet files | 1.1× |
| Read from S3 | 1.5.5 | 2.0 alpha |
|---|---|---|
| One 2.2 GB Parquet file, one column | 18.8 s | 7.7 s |
| 23 Parquet files, 13.6 GB, one column | 11.8 s | 3.9 s |
| One 1.7 GB CSV file | 116 s | 55 s |
| 30 Parquet files, about 1 MB each | 3.7 s | 3.3 s |
The read-ahead is controlled by read_ahead_depth. The default, -1, sizes it from the thread count; 0 gives you the 1.5 behaviour back, which is the quickest way to tell whether a regression comes from this change.
SET read_ahead_depth = 0; -- 1.5-style reads, for comparisonSET read_ahead_depth = -1; -- the 2.0 defaultKeep evicted remote data on local disk
The external file cache now spills to the temp directory instead of dropping blocks it has no memory for. With a 300 MB memory limit, MotherDuck's second read of an 854 MB Parquet file on S3 fell from 23.9 s to 0.35 s, because the blocks came off local disk instead of being downloaded again.
SET external_file_cache_spill = true;If you run DuckDB in a small container that reads the same remote files repeatedly, this setting may matter more than anything else in the release. Make sure the temp directory sits on a disk with room for it.
Recursive CTEs stop rescanning the table
In 1.5.5, every round of a recursive CTE re-reads the whole table it joins against, so the cost is the number of rounds times the size of the table. In 2.0, the table is read once, a lookup on the join column is built once, and each round only looks up the rows the previous round found (MotherDuck). The engine behind it was rewritten (DuckDB).
DuckDB's own reachability benchmark over one million edges went from 4.90 s in 1.5.4 to 0.12 s, about 40×. MotherDuck walked the ancestry of a 20,000-commit repository: 1.8 to 16 seconds across runs on 1.5.5, 0.10 s on every run with the alpha.
WITH RECURSIVE ancestors(id) AS ( SELECT max(id) FROM commits -- HEAD UNION SELECT p.parent_id FROM ancestors a JOIN commit_parents p ON p.commit_id = a.id)SELECT count(*) FROM ancestors;The gain grows with depth. A shallow hierarchy, such as an org chart a few levels deep, runs few rounds and has little to save. A deep one, like commit history, a bill of materials or a permission graph, is where you will see it.
Carry a value through the recursion with USING KEY
USING KEY keeps one row per key and lets you aggregate over the rounds, so iterative algorithms can be written in SQL. The example from the DuckDB preview averages b per a as the recursion counts down:
WITH RECURSIVE tbl(a, b) USING KEY (a, avg(b)) AS ( SELECT 1, 5 UNION ALL SELECT a, b - 1 FROM tbl WHERE b > 0)TABLE tbl;It returns one row, a = 1, b = 2.5. Reach for it when the recursion carries a cost or a depth, such as a shortest path, rather than just a set of ids.
VARIANT stores JSON as columns you can skip
VARIANT arrived in 1.5; 2.0 is the release where it pays off. When DuckDB checkpoints, it looks for fields that appear consistently with the same type and stores them as real sub-columns, a process called shredding. Rare or mixed-type fields go into a binary remainder (DuckDB). A filter on payload.type then reads one compact column, and the filter is pushed into the scan.
MotherDuck stored five million events three ways (as a JSON string, as VARIANT, and as a normal table with one typed column per field) and ran three queries:
-- Q1: filter on two fieldsSELECT count(*) FROM evWHERE payload.type::VARCHAR = 'purchase' AND payload.user.country::VARCHAR = 'FR'; -- Q2: sum a numeric field, groupedSELECT payload.user.country::VARCHAR AS country, sum(payload.props.amount::DOUBLE) AS amountFROM evWHERE payload.type::VARCHAR = 'purchase'GROUP BY ALLORDER BY 1; -- Q3: look inside a listSELECT count(*) FROM evWHERE list_contains(payload.tags::VARCHAR[], 'c');Source: MotherDuck, Why DuckDB 2.0 is faster (5 million events)
| JSON string | VARIANT, 2.0 alpha | Typed columns | |
|---|---|---|---|
| Q1 filter | 366 ms | 63 ms | 52 ms |
| Q2 sum by country | 408 ms | 61 ms | 51 ms |
| Q3 list contains | 357 ms | 1,960 ms | 67 ms |
On the field filters, VARIANT is about six times faster than the JSON string and within a few milliseconds of a hand-designed table. On 1.5.5 the same VARIANT queries took around five seconds each, so if you tried the type on 1.5 and gave up, try it again. It is also smaller on disk:
| Stored as | On disk |
|---|---|
| JSON string | 224 MB |
VARIANT, 2.0 alpha | 85 MB |
| Typed columns | 45 MB |
Q3 is the exception. Casting a VARIANT list to VARCHAR[] costs about two seconds in this alpha, so the JSON string is roughly five times faster for that query. If a query you run often looks inside arrays, keep that field as JSON or promote it to its own column.
ALTER TABLE ev ADD COLUMN type VARCHAR;UPDATE ev SET type = payload.type::VARCHAR;Promoting the fields you filter on most is still the fastest option. VARIANT is what you use for everything you have not promoted yet.
DuckDB can run as a server
The quack extension, DuckDB's own network protocol, becomes stable in 2.0. Any DuckDB process can serve its databases, and another DuckDB can attach to it and send queries with CONNECT; the query runs on the server and the results stream back (DuckDB).
CALL quack_serve(token = 'my_token');ATTACH 'quack:server.example.com' AS qk (TOKEN 'my_token');CONNECT qk;SELECT count(*) FROM events;DISCONNECT;This addresses a common limit of an embedded engine: one process per user, each with its own cache and its own copy of the work. It relies on DuckDB's existing multi-connection transactions (InfoQ). CONNECT also accepts a PostgreSQL or MySQL URL, and a new optimiser sends the SQL to that server instead of pulling whole tables across the network.
The preview shows a token and nothing else for authentication. Until the release documentation says more about TLS and access control, run a quack server on a private network, as in the diagram, not on a public address.
What will break
The DuckDB team lists only a few breaking changes so far and leaves the full list to the release announcement (DuckDB):
- The default storage format becomes v2.0.0. This is the one to plan for: a database file written in the new format is a migration, not a version bump.
- The lambda syntax transition is completed. Expect the deprecated form of lambdas in list functions to stop working, and check the release notes for the exact cut.
- A new PEG parser replaces the PostgreSQL-derived one. It is designed to be compatible, but unusual SQL is exactly what a new parser trips on.
- ICU is gone. Time zones, calendars and collations are now implemented natively in the
icuextension.
Smaller additions worth knowing
Triggers arrive with BEFORE and AFTER, per-row and per-statement modes, and transition tables. INSERT, UPDATE, DELETE and COPY now work inside a CTE, which turns a staging-to-archive move into one atomic statement:
WITH moved AS MATERIALIZED ( DELETE FROM staging RETURNING *)INSERT INTO archive SELECT * FROM moved;There are also nested schemas (finance.reports.q3), session variables (SET VARIABLE threshold = 100; then $threshold), json_set and its siblings for changing JSON in place, and an APPROX NEAREST join for top-k similarity search over embeddings.
What to test before 2.0 ships
- Install the alpha CLI and run your slowest real queries against the same files: remote Parquet first, then anything recursive.
- For a regression on remote reads, rerun with
SET read_ahead_depth = 0;to see whether read-ahead is the cause. - Find queries that filter JSON fields and try them on a
VARIANTcopy of the table; check any that look inside arrays. - Turn on
external_file_cache_spillif you read the same remote files under a tight memory limit. - Open a copy of each
.duckdbfile with the alpha, never the original. - Search your SQL for the old lambda syntax and for anything else the parser might read differently.
- If a query errors, report it with a reproducible example in duckdb/duckdb while there is still time for a fix to land.
The remote-read and recursive-CTE gains need no change to your SQL, so they are free once you upgrade. The storage format is the part that is hard to undo; test that one first.