Why PostHog left DuckDB, and why MotherDuck would not have saved it
A summary of PostHog's posts, plus answers to two questions the posts skip: why not DuckDB on DuckDB, and why not MotherDuck.
The problem was that DuckDB runs on one machine. Hosting it somewhere else does not change that. PostHog hit three walls: single-node sizing, a DuckLake catalog that did not scale, and the Postgres protocol layer. MotherDuck takes over running the servers, but it fixes none of the three.
Timeline
-
Before 2026
ClickHouse
A shared, multi-tenant ClickHouse cluster. Modelling queries run for hours or days and break when nodes restart. No cost-based optimizer. Lake format support was slow to mature.
-
Jun 29, 2026
DuckDB + DuckLake
One DuckDB instance per customer, DuckLake as the catalog with data in S3, and Duckgres, a Postgres wire protocol server, so dbt, BI tools and
psqlwork. -
Nov 2, 2026
Trino + HogLake
Their own flavour of Trino and a new open table format, HogLake. Tested at PostHog's petabyte scale first, then design partners.
The three walls they hit
DuckDB sizing
- The worker size has to be picked before the query arrives.
- Queries were heavier than expected, so they needed huge instances.
- Spilling to added disks would make queries too slow and expensive.
- Slow S3 reads until async fetching landed recently.
- No way to spread one database across machines.
DuckLake at 2 PB
- About 1 trillion rows and 15.3 million snapshots.
- A one-table write took 3+ minutes. Each commit re-read stats for the whole catalog.
- Over 99% of stats rows were for deleted tables. Cleaning gave 30 to 50 times faster writes, then they grew back.
- Must stay compatible with the official schema, so deep tuning was blocked.
- Many connections to shared Postgres. They ended up carrying a fork.
Speaking Postgres
- Long-lived connections are harder to scale than HTTP.
- Little load-balancing tooling for pgwire. HTTP headers route better in Kubernetes.
- Every client expects its own niche Postgres behaviour.
- Coverage "never got good enough".
Why not DuckDB on DuckDB?
Not in the postsPostgres had two separate jobs here, and neither was the query engine:
- The front door (Duckgres). Customers connected as if to Postgres, because most data tools speak Postgres. DuckDB ran the queries underneath.
- The DuckLake catalog database. A native
.duckdbfile allows only one writer process. PostHog had one instance per customer plus their own 2 PB, so many processes write at once. DuckLake's docs recommend Postgres for that case, and PostHog already ran Postgres everywhere.
So a DuckDB engine on a DuckDB file makes the main problem worse: the engine on one machine was the weak point, not the storage.
Why not MotherDuck?
Not in the posts- Same limits. MotherDuck still runs each instance as single-node DuckDB. Even the biggest instance has to be chosen before the query runs. That is the sizing wall.
- Same catalog. MotherDuck's hosted DuckLake would keep the catalog problems.
- Business. PostHog sells the warehouse as a product. Building it on MotherDuck means a vendor bill per customer and less control. They already needed their own DuckLake fork because fixes did not land fast enough.
MotherDuck takes over running the servers. It does not fix single-node sizing, the catalog, or the Postgres protocol.
The new tech, from their GitHub
Open sourceThe blog post gives no detail, but both pieces are public repos with real docs.
- PostHog/hoglake (MIT, Kotlin, started Sep 2026). "Petabyte-scale open table format, an evolution of Ducklake and Iceberg." Metadata is rows in Postgres, data is Parquet in S3.
- PostHog/trino. A fork of Trino, merged with upstream every night. It adds
trino-hoglakeandtrino-ducklakeconnectors, and removes Iceberg, Postgres, MySQL and other connectors they do not use.
What HogLake changes vs DuckLake
- The catalog is a service. In DuckLake, every client embeds the catalog logic and writes to the metadata database directly. In HogLake, clients call a REST API and only the server touches Postgres.
- Commit cost does not grow with the catalog. A commit reads only the tables it writes. That fixes the 3-minute writes.
- Postgres only, on purpose. Foreign keys with cascade delete, advisory locks and partial indexes. DuckLake must stay portable across databases, so it has none of these. That fixes the stats rows of deleted tables.
- Maintenance runs on the server. Compaction and snapshot expiry are server jobs under locks, not procedures that clients run against each other.
- Extras: deletion vectors, row ids that are never reused, and a change feed with consumer offsets.
- Not compatible with DuckLake in any way, and not meant to be. They keep a DuckLake defect ledger with the evidence.
Why not just Iceberg?
The README says it plainly: "Iceberg buys ubiquity: every engine already reads it and there is no service to operate." HogLake trades that for metadata in indexed SQL instead of many object-store round trips, plus one place that owns commit, compaction and expiry. An Iceberg REST facade is designed, so other engines can read it later.
Status
- Deployed as "Gigahog" in dev, prod-us and prod-eu. The latest release is 1.4.7 (Oct 24, 2026).
- Clients: the Trino connector, a Python client (
pyhoglake) and a DuckDB extension. So DuckDB stays usable as a client, just not as the warehouse engine. - Authentication is off by default.
The benchmark numbers in the README (flat commit latency, about 380 commits/s) come from a laptop, so they show the shape, not production capacity.