Postgres for everything hits a wall the moment dashboards get concurrent
DEV Community

Postgres for everything hits a wall the moment dashboards get concurrent

Postgres for Everything Hits a Wall the Moment Dashboards Get Concurrent

I used to say, "Just use Postgres." After that, our dashboards became concurrent, and the read replica we quickly added to offload some of the pressure from the main database began overheating instead.

The meme that traps you
There is a reassuring notion that Postgres can handle all tasks. OLTP, queues, search, analytics, everything in a single package. But it shouldn't be the ultimate goal. MotherDuck was direct in their analysis from July 2026. As soon as you start providing real analytics, the "Postgres for Everything" philosophy becomes a structural inconsistency.

Typically, what most folks would do is they would probably get a read replica, isolate those heavy reads, protect the primary, and you're done. Except that's not what happens.

A Read Replica Does Not Fix the Physics

A read replica is a duplicate of the identical row-oriented storage. In May of 2026, the engineering team at ClickHouse put it best in their guide "While being beneficial in terms of high availability, performance, and horizontal read scalability, Postgres read replicas share the row-oriented limitations of the primary. They don't compress data effectively for analytics, and they still require heavy B-tree indexes that bloat memory."

The Challenge Lies in the Physics

Postgres specifically stores data in pages of 8 KB, with a tuple header of 23 bytes fixed per tuple. This implies that a dashboard query, for example, will pull in an entire row even if it only needs 2 columns. It's reading the entire row for a SELECT that only focuses on a fraction of it. When you multiply that by the level of concurrency, the situation quickly becomes very difficult to deal with.

Where It Actually Breaks

Looking back on a September 2026 data engineering incident, a retrospective analysis noted that a replica had been hit by "hundreds of similar aggregations running at once," causing both CPU and memory pressure to spike. WAL replay and fast queries were vying for the same resources, causing fast queries to become "painfully slow". That's the part that gives it away. It's not a single slow query, it's the accumulation of them.

The Accumulation of Slow Queries

MotherDuck mimicked the curve as well. A dashboard aggregation that required 50ms when you were just starting out, increases to 5 seconds when you go into the tens of millions of rows. That same aggregation eventually times out once concurrent load piles on. And your latency budget for customer-facing analytics? Anything under 100 milliseconds, and users perceive it as instant. Five seconds may not be immediate, but it sure is faster than a support ticket!

The Hidden Cost of Mixing Workloads

Another cost people tend to overlook is the one pointed out by Brandur Leach and Gunnar Morling: mixing queue-like workloads with normal OLTP workloads in Postgres causes MVCC bloat, index fragmentation, and WAL pile-up. You are not merely slow. You are gathering rot.

The Expensive Band-Aid

Teams are forced to compensate for the limitations of a row store by investing heavily in hardware. This is the reason why you eventually over-provision an AWS r8gd.4xlarge to handle some dashboard spikes. It's just there mostly idle at 3am, burning money, but you know it must do it. You are paying the highest cost for a tool that is not meant to do that particular job. Increasing the size does not solve the problem of having the wrong format. It simply means you pay for it later.

The Fair Thing to Do

The fair thing to do is to stop expecting the row store to behave like a column store. Where I landed All of these reasons do not imply that Postgres is inferior. I still choose to store my transactional data in Postgres, and I will defend that choice. Asking one engine to be both the ledger and the analytics warehouse is the mistake. They're different shapes of problem. The solution does not involve creating a larger replica. Instead, a column-oriented serving layer designed for aggregations such as ClickHouse is used. This layer compresses data significantly and specifically accesses only the columns required for processing a query.

Choosing the Right Tool for the Job

โ†’ Read replicas isolate compute, not storage format
โ†’ Row headers plus 8 KB pages force full-row scans for narrow queries
โ†’ Concurrency turns "fine" into "timeout" past tens of millions of rows
โ†’ Over-provisioning is a tax, not a solution

Choosing Postgres for all kinds of data storage in your platform is like being madly in love. Nevertheless, everything comes at a cost. So here's my question for you. What was the exact row count or concurrency level where you finally had to throw in the towel and admit Postgres alone wasn't ever going to handle your analytics workloads?

Read on DEV Community ↗ ← Back to News

Comments

No comments yet. Start the discussion.