The Chapter That Undoes the First One

Three ways past the single-writer lock — a DuckLake whose catalog choice decides everything, DuckDB actually serving queries over TCP to another process, and the managed service this book could not run.

Chapter 1 opened on a clean claim: DuckDB is in-process, single-machine, and has no server. That is the whole design, and every chapter since has been a consequence of it.

It is also, as of the 1.5 series, no longer the complete story.

DuckDB now ships an extension that turns it into a server other processes connect to over TCP. It ships a table format whose concurrency depends entirely on a configuration choice most people will make without thinking. And there is a managed cloud service built on the same engine. None of that was true in a from-memory account of DuckDB, and it changes the answer to the question chapter 16 left open.

This is the fastest-moving material in the book, and it is worth saying so at the top rather than the bottom. Everything below was run against DuckDB 1.5.5 in August 2026.

DuckLake, where the catalog is the whole decision

Chapter 6 introduced DuckLake: data files as ordinary Parquet, metadata in a SQL database. The obvious question is whether that fixes the lock, and the answer is it depends on which database, more sharply than the documentation suggests.

Start with the default. Three processes attach the same DuckLake and each insert 50 rows:

attach 'ducklake:lake2/meta.ducklake' as lake (data_path 'lake2/data/');
writer 2 OK
writer 1: IO Error: Failed to attach DuckLake MetaData … Could not set lock on
          file ".../lake2/meta.ducklake": Conflicting lock is held … (PID 98424)
writer 3: IO Error: Failed to attach DuckLake MetaData … Conflicting lock

Two of three could not even attach. Final table: 50 rows, from one writer.

That is chapter 16’s lock, unchanged, and the reason is sitting in the path. meta.ducklake is a DuckDB database file. Putting your metadata in a SQL database helps only if that database can handle concurrent writers, and a DuckDB file is precisely the thing that cannot.

DuckLake accepts other catalogs, and the URI says which:

attach 'ducklake:sqlite:lake4/meta.sqlite' as lake (data_path 'lake4/d/');

Same three writers, same 50 inserts each:

writer 1: 37 inserted, 13 failed
writer 2: 39 inserted, 11 failed
writer 3: 44 inserted,  6 failed

per writer: [(1, 37), (2, 39), (3, 44)]
total     : 120

All three attached. All three wrote. All three landed rows in the same table. That is genuinely concurrent multi-process writing, which nothing else in this book has managed.

And 30 of 150 inserts failed:

TransactionContext Error: Failed to commit: Failed to commit DuckLake transaction.

That is optimistic concurrency control, and it is a different contract from the file lock. The lock refuses you at the door; optimistic concurrency lets everyone in, does the work, and rejects the commits that conflicted. Nobody is blocked, nobody waits, and roughly a fifth of the work has to be done again.

Which means the same obligation as chapter 16, in a new place: retry is your job. A writer that does not catch a commit failure and try again will silently drop a fifth of its inserts on a contended table. The failure is loud — it is an exception — but only if something is looking.

For a real deployment the catalog would be Postgres or MySQL rather than SQLite, and that is the intended shape. The rule generalises cleanly: DuckLake’s concurrency is exactly the concurrency of the database you pointed its catalog at. Choose that on purpose.

Quack, which is a server

The 1.5.3 release added an extension that does the thing this book has spent fifteen chapters saying DuckDB does not do.

load quack;
call quack_serve('quack://localhost:4213', disable_ssl := true);
('quack://localhost:4213', 'http://localhost:4213', '93C7C58D4B4CAD80A7F51B788638687D')

That process is now listening on a TCP port. The third value is a token, generated on the spot, and it is required — a client without one gets:

Invalid Input Error: Could not find a Quack authentication token

There is a second guard worth quoting, because it is unusually opinionated for a default:

Invalid Input Error: Only localhost is allowed as a Quack RPC hostname by default,
set allow_other_hostname=true to override. We strongly recommend reverse-proxying…

Binding to 0.0.0.0 is refused unless you insist. Good.

Now a completely separate process, connecting over the network:

select * from quack_query('quack://localhost:4213',
                          'select count(*) c, sum(v) s from t',
                          disable_ssl := true, token := '93C7…');
[(1000, 1498500)]

A DuckDB client queried a DuckDB server over TCP and got rows back. Then the part that matters:

client before : [(1000,)]
-- insert into t values (9999, 1)
client after  : [(1001,)]
server sees rows: (1001,)

The client wrote, and the server’s own view agrees. A second process modified a database it does not hold the lock on.

And the lock is still exactly where it was. While the server ran, a direct file open was refused:

IO Error: Could not set lock on file ".../qs.duckdb": Conflicting lock

That is the design in one observation. Quack does not remove the lock — it puts a server in front of it. One process holds the file, as it always did, and serves everyone else. This is the classic client-server architecture, arrived at from the opposite direction. Instead of a database that comes with a server, an embedded database that can grow one when a workload needs it.

Which is a genuinely appealing shape. Nothing changes for the single-process case, so you pay nothing until the day two processes need the same data — and then it is load quack rather than a migration.

The obvious caveats, none of which the run contradicts. The server is a single process, so it is a single point of failure and a single machine’s worth of capacity. Auth is a shared token, which is not a user model. And this is a young extension whose API will move.

MotherDuck, which this book did not run

MotherDuck is a managed service from the company behind DuckDB. The pitch is a hybrid. A cloud warehouse that speaks DuckDB, where one query executes partly on their infrastructure and partly on your laptop, against a mixture of their storage and your local files.

The extension is real and installs:

install motherduck; load motherduck;
attach 'md:';
Invalid Input Error: Cannot connect to MotherDuck server: no token provided.

That is where verification stopped. ATTACH 'md:' opens a browser SSO flow, and this book does not sign into third-party services to write a chapter. So: the extension exists, it loads, and it demands credentials, all of which was run. Everything about what it does once authenticated is not verified here and should be read as the vendor’s description rather than this book’s measurement.

The architectural idea is worth understanding regardless, because it is the one thing in this chapter that genuinely leaves your machine. DuckLake and Quack both keep the data where you put it. MotherDuck is a different bargain: storage and compute that someone else operates, reached through the client you already use.

Choosing

Four options, in order of how far they take you from chapter 1:

Stay local. One writer, many readers, files where they sit. Most analytical work never needs more, and everything before this chapter applies unchanged.

DuckLake with a server-backed catalog. Several processes writing one analytical dataset, data staying in your own object storage, and no service to run beyond a database you probably already have. Budget for retries — a fifth of the commits failed under contention here.

Quack. Several processes needing one DuckDB database, on one machine or one network, with the least change to everything else. The lock stays; a server fronts it.

MotherDuck. You want someone else to run the storage and the compute. Evaluate it on its own terms, not on this chapter’s evidence.

The one thing not to do is reach for any of these before the local model has actually failed. Every option here adds a component that can be down, and the single-writer model is a deliberate exclusion rather than a bug, as chapter 16 argued. Three of these four are answers to a problem most readers will not have.

What will be stale first

This chapter, all of it, and sooner than the rest of the book.

Quack shipped in 1.5.3, three releases ago. DuckLake is younger. Both are moving fast enough that the function signatures above are a snapshot rather than an API. MotherDuck is a commercial product whose capabilities change on its own schedule and were not tested here at all.

Treat the mechanics as the durable part: a catalog is only as concurrent as the database holding it; a server in front of a lock is still a lock; a managed service is a different trust boundary. Those will outlast every name on this page.

Final thoughts

The honest reading of this chapter is that DuckDB’s founding constraint has become a default rather than a boundary. It is still in-process, still single-writer, still a file — until you decide otherwise, and then there are three ways out that did not exist two years ago.

The measurement worth keeping is the DuckLake one, because it is the mistake most available to make. ducklake:meta.ducklake and ducklake:sqlite:meta.sqlite look like the same feature and are not. One refused two of three writers at the door. The other let all three in and rejected a fifth of their commits. Nothing in the syntax warns you which you picked.

And keep the shape of the Quack result: a second process wrote to the database, and the file lock never moved. That is not the lock being fixed. It is a server being put where a server always goes.

Next: Files to Answers — one workflow, end to end, using everything in this book.

Comments