Software Development

Moving to Postgres was a Mistake

Three painful problems that will wreck your app

Developers love Postgres because the top half feels elegant.

The types are nice, the planner is smart, and the ergonomics are great.

But that doesn’t matter when the bottom half — the storage engine — starts dropping in performance. If your workload churns rows, Postgres quietly taxes you until your disks bloat.

This article will therefore outline and evaluate three fundamental problems with Postgres, which become obvious later on in development.

So without further ado, let’s dive right in!

Problem 1, Index Bloat

Every time you change data, you also change indexes.

In Postgres, those index entries are versioned right along with the rows, so dead tuples pile up inside the tree.

Over time, your index becomes loose. The result is high CPU and rising latency, even when the logical row count barely changes.

Bloat is when the physical size grows larger than the live data requires, because dead tuples and empty gaps persist.

Indexes suffer the most, since point lookups still traverse pages that no longer carry value.

Convex ran into this the hard way as you will see in the video.

Their workload created constant in place updates and deletes, which is exactly the shape that maximizes index churn:

Problem 2, Poor Space Reclamation

MVCC is great for avoiding read-write lock fights; it is not great for reclaiming space under churn.

Postgres leaves dead tuples in tables and indexes until maintenance catches up.

Now, Autovacuum helps, but only when it fires at the right time, with the right intensity, and with parameters.

Convex admitted they were racing compaction to free space, causing queries to waste CPU time.

Problem 3, An Inefficient Storage Engine For Churn

Here is the uncomfortable part.

Postgres shines in the top half with its types and planner. The bottom half, the storage engine, struggles under update-heavy transactional load.

MySQL’s InnoDB takes a different path with its “redo log” and page handling, and it has been tuned for years for exactly this situation.

It does not pretend that dead rows are history. It reuses space, flushes changes through a redo-log, and compacts pages periodically.

And this path is significantly more storage-efficient at scale compared to the approach of postgres.