Skip to content

When Your DuckDB File Needs a Second Reader

6 min readDuckDBAnalyticsDatabases

The file is called q3_final_v2.parquet and it is in a Slack thread from nine days ago. Someone is asking whether it includes the refunds fix. You do not remember, because the DuckDB database that produced it is on a laptop that is currently closed and in a bag.

That is what outgrowing a local database looks like for an analytics project, and it is a different shape from the one app developers hit. Nobody is losing writes. Nothing crashed on deploy. The data is fine. It is just unreachable by everyone except you, so it gets copied instead of queried, and the copies immediately start disagreeing.

Three parts, and only one of them needs an address

Pull an analytics project apart and you get:

The sources. A warehouse export, a bucket of parquet, a vendor CSV, an API you page through at 2am. These already live somewhere. They are not yours to host.

The pipeline. The notebook or the script that turns sources into tables. It wants your CPU, your credentials, your half-broken virtualenv, and the ability to be rerun from scratch when you find a bug in the refunds logic.

The result. Six or seven derived tables in analytics.duckdb that people ask questions about.

When an app graduates from an embedded database, all of it moves, because in an app the database is the state. Analytics does not work that way. The pipeline is authoritative and the file is its output. So the question is not "how do I migrate this project", it is "which artifact needs to be reachable". It is the result, and only the result.

That is good news, because the result is one file.

Moving the file

A DuckDB database is a single self-contained file. Its storage format starts with a checksum, then four magic bytes spelling DUCK, then a storage version number. Everything else about the database, schema, data, and indexes lives behind that header in the same file. There is nothing beside it to remember to copy, which is exactly why it travels well.

From a terminal, version 1.4.0 of the Layerbase CLI takes it in one verb:

bash
npm i -g layerbase
export LAYERBASE_API_KEY=sk_...

lbase promote ./analytics.duckdb --yes
text
Promote ./analytics.duckdb (duckdb) to a new cloud duckdb database named "analytics".
Creating cloud database "analytics" (duckdb)...
Uploading dump...
Restoring...

Promoted ./analytics.duckdb (duckdb) into "analytics" (18.2 MB).

It reads the header rather than trusting the extension, so a file you named warehouse.db three months ago is still recognized as DuckDB, and a file that is not a DuckDB database fails saying so instead of uploading garbage. --yes is required when there is no terminal attached, which is what makes it safe to put in a script. The one-command post covers the rest of that verb.

If you would rather see the database before you fill it, the dashboard does the same job. Create one at layerbase.com/create/duckdb, open its query console, and use Import, then Restore full database. It accepts a .duckdb file, replaces everything currently in the database, and makes you type the database's name first, because a whole-file swap is not something to trigger with a stray click. Uploads are capped at 100 MB per file, which is a lot of derived tables and not very much raw event data.

The part that surprises people

DuckDB has no network protocol. It is a library that opens a file, so there is no port to expose and no client-server mode to turn on. To make a hosted DuckDB reachable over TCP with TLS, Layerbase runs it behind a PostgreSQL-compatible proxy on port 5432. Your connection string looks like this:

bash
psql "postgresql://layerbase:PASSWORD@your-host.cloud.layerbase.dev:5432/analytics?sslmode=require&sslnegotiation=direct"

So the duckdb CLI will not reach it, and neither will a DuckDB client library. What will reach it is psql, any Postgres driver in any language, and the Postgres connector in whatever BI tool your teammate already has open. For a dashboard, that is usually the better half of the trade: nobody has to install anything.

The SQL you send is still DuckDB SQL. information_schema and most pg_catalog introspection work, so clients can browse tables and columns normally. One wart worth knowing before you see it: the proxy materializes its compatibility shims as real tables, so a raw \dt shows __duckgres-prefixed bookkeeping tables next to yours. The in-dashboard console filters them out. They are safe to ignore. The full picture is in DuckDB on Layerbase.

Republish, do not migrate

Because the pipeline stayed local, the ongoing rhythm is not sync. It is publish.

You rerun the pipeline, get a fresh analytics.duckdb, and push it again with the same restore that put it there the first time. Whole-file swap, every time, which sounds crude until you notice it is the only version of this that cannot drift. There is no reconciliation step to get subtly wrong, and the thing on your laptop stays the source of truth.

Two shortcuts are worth knowing. If your source data is already at a URL, the hosted database can read it in place rather than waiting for an upload:

sql
CREATE OR REPLACE VIEW sales AS
SELECT * FROM read_parquet('https://example.com/sales.parquet');

And single tables do not need a full restore. The console's import takes CSV, TSV, JSON, NDJSON, or parquet and turns it into a table directly, same 100 MB ceiling.

Going the other way is just DuckDB. COPY (SELECT * FROM tbl) TO 'result.parquet' (FORMAT parquet) still produces the extract someone wants attached to an email, and exporting the database from the dashboard hands you back a plain .duckdb file that opens in local DuckDB. Nothing about the hosted copy is a format you have to escape from.

One version note. Layerbase provisions DuckDB 1.5. DuckDB has guaranteed backward compatibility in its storage format since 0.10, so a file written by an older DuckDB opens on a newer server without ceremony. Forward compatibility is documented as best effort, so if your laptop is running ahead of the server, check the version before assuming the upload will land.

When the parquet attachment was fine all along

Plenty of analysis should never be hosted, and the file at the top of this post is not automatically a mistake.

A one-off number for a board deck is a deliverable, not a database. Send the parquet. Analysis whose only consumer is you should stay where it is; an in-process engine reading local files is faster than anything with a network in front of it, and that is the case DuckDB was built for. Data that cannot legally leave your machine does not get an exception because sharing would be convenient.

The tell is repetition. If the question gets asked once, export it. If the question has a v2 in its filename, something on a schedule needs to read those rows, or a teammate is going to have a follow-up, then the copies are already costing you more than the hosting would.

DuckDB is on the free tier, no card, and a free database sleeps after an hour of inactivity and wakes on the next connection in a second or two, which is the correct behavior for a dashboard nobody opens on weekends. You also get a branch per database, so trying a schema change on a copy is cheap. The lifecycle docs spell out the rest of the sleeping and waking rules.

Create a DuckDB database and push the file, or if you landed here comparing hosted DuckDB options, MotherDuck's 2026 pricing changes cover the other end of that market.