Zephiel API
Engineering11 June 20248 min read

Postgres is still the answer

Nine years, four serious proposals to move part of the workload elsewhere, and one that we actually went through with.

We store almost everything in Postgres: accounts, subscriptions, keys, invoices, usage, and until recently every request event. Four times we have seriously proposed moving part of that somewhere more specialised. Once we did it. This is the accounting.

The three we did not do

A document store for provider response schemas, in 2017. The argument was that schemas vary per provider and relational modelling would be awkward. It would have been, but jsonb columns handle it, they are indexable with GIN, and they do not require a second database with its own backups, its own failover, and its own on-call knowledge.

A dedicated search cluster for the catalogue, in 2019. With forty listings, Postgres full-text search returns results in under ten milliseconds. We would have been operating a search cluster to serve a query over a few hundred rows.

A key-value store for rate limit counters, in 2021. This one was closer — counters are the workload relational databases are worst at, and we did end up putting a cache in front. But the durable record stayed in Postgres, because a rate limit counter that vanishes on failover is a rate limit that does not exist.

The one we did

Request events. We were writing every gateway call as a row and keeping it forever. By 2022 that table was the largest thing we had by two orders of magnitude, and it made every operation on the database slower — backups, restores, schema changes, vacuum.

The fix was not a different database. It was recognising two workloads in one table: recent events, queried constantly for dashboards, and historical events, queried rarely for billing disputes. Recent events stay in a hot table with a two-day window. Everything older is rolled into daily aggregates and the raw rows are dropped.

The hot table is now small enough to fit in memory. Queries that took eight seconds take forty milliseconds. We did not add a database; we deleted data we were never going to read.

Why it keeps being the answer

One backup story. One failover story. One set of credentials, one query language, one thing to be woken up about. Every additional datastore is not just its own operational burden but a consistency problem at the boundary.

Postgres will not be the answer forever. It has been the answer for nine years, and every time we proposed leaving, the actual problem turned out to be a schema or a query rather than the database.

Keep reading