Is DuckDB safe for production? The honest limitations

By Arshad Ansari

"Is DuckDB safe for production?" is the right question asked slightly wrong. DuckDB is not a scaled-down toy that becomes safe once you're brave enough. It's a database with a specific concurrency and durability model, and it is completely safe inside that model and genuinely unsafe outside it.

So the useful version of the question is: safe for which workload? Here's what actually bites, in the order it bites people.

The single-writer model is the whole story

DuckDB allows one writing process at a time against a database file. Multiple threads inside that process are fine. Many processes can read the file at once, as long as none of them is writing. Two processes both opening the file for writing are not.

This is not a bug or a temporary limitation — it's the design. DuckDB is an in-process database, like SQLite. There is no server arbitrating between clients, because there is no server.

Almost every "DuckDB isn't production-ready" story I've heard traces back to this. Someone runs the ETL job on a schedule, someone else points a dashboard at the same file, and eventually the two overlap. What you get is a lock error, or — if you were clever enough to copy the file to dodge the lock — a reader seeing a half-written state.

The design that avoids it entirely:

                    ┌─ writer job ──► Parquet partitions on S3
                    │                   (immutable, append-only)
  source systems ───┤
                    └─ readers ────► DuckDB, N processes, read-only

Writers never share a mutable file. They produce immutable Parquet partitions. Readers open those partitions read-only, as many processes as you like, with no coordination at all. The concurrency problem disappears because you removed the shared mutable state, not because you managed it better.

If you must use a .duckdb file with concurrent readers, open them explicitly read-only:

import duckdb

# Safe: many of these can run at once against the same file.
con = duckdb.connect("analytics.duckdb", read_only=True)

Read-only processes share the file with each other but not with a writer: while any process holds it read-write, the others cannot open it at all. That is why the writer should produce Parquet rather than update the file the readers use. DuckDB will also refuse a write through a read-only connection rather than corrupt anything.

Durability: it's ACID, but know what that covers

DuckDB is ACID-compliant with write-ahead logging. A transaction that commits is durable; a crash mid-transaction rolls back cleanly. On this axis it behaves like a real database, because it is one.

What people actually get wrong is what sits around the transaction:

  • The file is a single point of failure. ACID protects you from a crash. It doesn't protect you from a deleted file, a corrupted volume, or an ephemeral container's disk vanishing at the end of the run. If your database lives on a pod's local disk, it lives exactly as long as the pod.
  • A .duckdb file is not a backup format. Copying it while a writer is mid-transaction gives you a file whose contents are undefined. Snapshot by exporting (EXPORT DATABASE) or by treating Parquet as the durable layer.
  • Storage format compatibility. DuckDB reached 1.0 with a stability commitment, and files are forward-compatible within that line — but a file written by a newer version isn't necessarily readable by an older one. Pin the version in production and upgrade deliberately, the same as any database.

The rule I use: Parquet is the durable artefact, the .duckdb file is a cache. If losing the file would be a data-loss incident rather than an inconvenience, the architecture is wrong, not DuckDB.

Memory is the failure mode you'll actually hit

DuckDB spills to disk when a query exceeds memory, so it doesn't simply die the way people expect. But the spill has to go somewhere, and the default temp location is often not where you want it in a container.

Two settings that belong in every production configuration:

SET memory_limit = '12GB';        -- leave headroom for the host process
SET temp_directory = '/var/tmp/duckdb';  -- a real disk with real space

Get these wrong and the classic production failure is a large join filling the container's ephemeral layer until the orchestrator kills the pod — which looks like a mysterious OOM and is actually a disk problem.

Set memory_limit well below the container limit. DuckDB accounts for its own buffers, not for the Python process holding your dataframes, the Arrow tables in flight, or the runtime itself.

The rest of the container setup — thread counts, preserve_insertion_order, Parquet layout, the read-only fan-out — is in running DuckDB on your own infrastructure.

Where DuckDB is genuinely the wrong choice

Be honest about these rather than working around them:

  • Many concurrent writers. Covered above. If the requirement is real and can't be redesigned into partitioned writes, use a database with a server.
  • Row-level security, roles, audit trails. DuckDB has no user model. It's a library reading a file — the security boundary is the process and the filesystem, not the database. In regulated environments this is usually decisive on its own. (For a multi-tenant product the workable pattern is isolation in the service that owns the file — how that looks for SaaS dashboards.)
  • High-concurrency interactive serving. Fifty analysts writing exploratory SQL simultaneously is a warehouse workload. One dashboard hitting a cached result set is not. Know which you have. The analyst crowd is a Snowflake question; thousands of customer-facing queries a minute over fresh data is a ClickHouse one.
  • OLTP. It's a columnar analytical engine. Thousands of small point updates per second is the workload it's worst at, and Postgres is right there.
  • Cross-region distributed queries. Single node means single node.

Where it's been safe, in my experience

Everything that is batch, single-writer, and reproducible:

  • Transformation jobs reading Parquet and writing Parquet — the largest share of most compute bills, and structurally free of the concurrency problem.
  • Embedded analytics inside an application that controls its own queries.
  • CI: running the real engine against real data in seconds, so tests exercise production SQL rather than a mock.
  • Local development against a copy of production Parquet, offline, with no cloud credentials.
  • Anything where the alternative was a cluster provisioned for a working set that fits in RAM.

The common thread: the workload has one writer and tolerates being re-run. If a job fails, you fix it and run it again, and the output is identical. That property makes the whole class of concurrency questions moot.

A short pre-production checklist

Before putting DuckDB on the critical path, I'd want yes to all of these:

  1. Exactly one process writes, never while readers have the file open; every reader is read_only=True.
  2. Parquet (or another durable store) is the source of truth; the .duckdb file can be rebuilt from it.
  3. memory_limit and temp_directory are set explicitly, with temp_directory on real disk.
  4. The DuckDB version is pinned, and upgrades are a deliberate change with a test run.
  5. Failure means "re-run the job", not "restore from backup".

That list is short because the model is simple. The danger isn't that DuckDB is fragile — it's that it's so easy to start using that people skip the design step they'd never skip for Postgres.

So: is it safe?

For batch transformation, embedded analytics, CI and development, DuckDB has been more reliable in my hands than the distributed systems it replaced, mostly because there's dramatically less to go wrong. One process, one file, no network partition, no cluster state.

For multi-writer, governed, high-concurrency serving, it isn't safe and won't become safe — that's a different tool's job.

The honest failure mode isn't corruption. It's discovering six months in that you needed the governance layer.

If you want the full argument with runnable code, that's my book, Local-First Analytics — DuckDB, Parquet and Arrow on hardware you already have. Related reading: what DuckDB is actually good at in production, when local-first runs out of road, and DuckDB vs Snowflake.

If you'd like someone to look at your specific workload and tell you honestly whether this design fits it, a Data Platform Audit is a week and a written roadmap you keep. The scoping call below is free.

Common questions

Is DuckDB safe for production?
It is completely safe inside its concurrency and durability model and genuinely unsafe outside it, which makes "safe for which workload?" the useful version of the question. Safe: one writer, analytical queries, Parquet in object storage as the source of truth, your own service in front handling auth and concurrency. Unsafe: several processes writing one file over a network filesystem, row-level security requirements, high-concurrency interactive serving, or OLTP. It is not a scaled-down toy that becomes safe once you are brave enough — it is a database with a shape.
What are DuckDB's limitations?
Five that actually bite. One process holds the database read-write at a time, so multi-writer designs need a service in front whether you planned one or not. There is no governance layer — no roles, no row-level policies, no audit trail; the file is the permission boundary. Memory is the failure mode you will actually hit: larger-than-memory work spills to disk, and the spill needs a real temp directory with real space. You build the serving layer yourself — auth, connection lifecycle, caching, rate limiting, metrics. And you scale up, not out: one machine is a hard ceiling rather than a soft one.
Does DuckDB support row-level security?
No. DuckDB has no user model at all — it is a library reading a file, so the security boundary is the process and the filesystem, not the database. There are no roles, no per-row policies and no audit trail to enable. The workable pattern is to enforce access in the service that owns the DuckDB file: your API knows who is asking and adds the tenant predicate before the query reaches the engine. In regulated environments the absence of a database-level user model is usually decisive on its own, and that is a reason to pick something else rather than a gap to paper over.
What memory settings does DuckDB need in production?
Two, and they belong in every production configuration. Set `memory_limit` well below the container limit — DuckDB accounts for its own buffers, not for the Python process holding your dataframes, the Arrow tables in flight, or the runtime itself. Set `temp_directory` to a real disk with real space. Get these wrong and the classic failure is a large join filling the container's ephemeral layer until the orchestrator kills the pod, which looks like a mysterious OOM and is actually a disk problem.

Does DuckDB fit your system?

The 16-question production-fit checklist I run before putting DuckDB on a critical path — writers, working set, durability, memory, and who talks to it. Each question comes with what a bad answer sounds like.

One email with the whole checklist. Nothing follows it. Reply and it reaches me, not a queue.

Want the whole playbook?

If this was useful, the long version is my book. Local-First Analytics — 314 pages, runnable code for every chapter — is the full build: DuckDB, Parquet and Arrow, from install to production. On Amazon, or request a free review copy.

Get the book

Not ready to buy? Read chapter 1 free — the whole chapter, no email required.

Rather talk it through? Book a free 30-minute call. No slot that suits your time zone? Email [email protected].