---
title: "When PostgreSQL Isn't the Right Fit: Recognizing Workloads That Need Different Architecture"
published: 2026-06-12T08:00:47.000-04:00
updated: 2026-06-12T08:01:04.000-04:00
excerpt: "Postgres handles 90% of workloads well. Here's how to tell if yours is in the 10% — and what the diagnostic query that confirms it looks like."
tags: PostgreSQL, Database
authors: NanoHertz Communications
---

> **TimescaleDB is now Tiger Data.**

When PostgreSQL isn't the right fit, the signs don't announce themselves clearly. Postgres is the right database for roughly 90% of workloads, such as SaaS backends, CRUD applications, and transactional systems with mixed read/write access on shared rows. But there's a narrow 10% where those same strengths become overhead: high-frequency append-only ingestion, time-ordered data accumulating at sustained rates, analytical scans over hundreds of millions of rows. If that sounds like your system, this post is for you.

## What You Will Learn

If you've added indexes, implemented partitioning, tuned autovacuum, and upgraded hardware only to watch performance degrade again on the same trajectory, the problem likely isn't your configuration. By the end of this post, you'll know whether your workload is in Postgres's 10%, how to confirm it with a single diagnostic query, and what the first concrete step toward the right architecture looks like.

## Why It Matters

An optimization problem and an architecture problem look identical in the early stages. Both show up as slow queries. Both respond to the same fixes: indexes, partitioning, autovacuum tuning, hardware upgrades. The divergence happens later, when the fixes stop holding and performance degrades on the same trajectory regardless of what you change.

This is what’s known as the [optimization treadmill](https://www.tigerdata.com/blog/postgres-optimization-treadmill): a predictable sequence of phases that each buy three to six months of relief without changing the underlying trajectory. [MVCC overhead](https://www.tigerdata.com/blog/mvcc-feature-youre-paying-for-but-not-using), row-oriented storage, B-tree index maintenance, WAL volume. These aren't bugs. They're architectural tradeoffs that work well for 90% of workloads and work poorly for the 10%.

Knowing which problem you have determines whether you should keep tuning or make a different decision.

## What Postgres Was Designed For

Postgres's architecture is built around concurrent access to shared rows. Multiple transactions read and write the same data at the same time, and MVCC handles the isolation. B-tree indexes find specific rows by key. Row-oriented storage assumes that when you retrieve a row, you want most of the columns in it.

For an e-commerce backend, a user authentication system, or a multi-tenant SaaS product, these are exactly the right tradeoffs. Transactions need isolation. Point lookups by user ID are the dominant query pattern. Write rates track user activity, which gives the database natural breathing room between peaks. The question isn't whether Postgres is good. It's whether the workload you're running matches the patterns its architecture was designed to serve.

## The Workload That Breaks the Match

Three characteristics, when they appear together, put a workload outside what Postgres handles well.

**Append-only or append-heavy writes.** Rows are written once and never, or almost never, updated. Sensor readings, financial transactions, log entries, event streams. Every row still pays the full [MVCC cost](https://www.tigerdata.com/blog/mvcc-feature-youre-paying-for-but-not-using): a 23-byte tuple header tracking transaction visibility, hint-bit dirtying on reads, and autovacuum running continuously to freeze tuples and update the visibility map. None of that overhead produces value on data that will never be touched again.

**Sustained high write rates.** Not burst traffic that settles. Continuous ingestion at thousands to hundreds of thousands of rows per second, around the clock. The table grows without pause, B-tree index maintenance adds overhead with every insert, and that cost compounds with row volume, so there is no quiet window for [autovacuum to catch up](https://www.tigerdata.com/blog/preventing-silent-spiral-table-bloat).

**Analytical query patterns.** The queries are aggregations over time ranges: averages, counts, percentiles, `GROUP BY` time bucket. Row-oriented storage forces Postgres to read all columns of every matching row even when the query needs two. On a 30-column table, that's fifteen times the I/O a [columnar layout would require](https://www.tigerdata.com/blog/hypercore-a-hybrid-row-storage-engine-for-real-time-analytics).

Any one of these is manageable. All three together is the combination that Postgres handles well at one million rows and struggles with at one hundred million.

## The Optimization Treadmill in Practice

The pattern is predictable. Queries slow down as the table grows. You add indexes, and reads get faster. Write performance drops because index maintenance scales with row volume. You upgrade the instance. Performance stabilizes and costs go up. You implement partitioning. Recent-data queries get faster. Partition management becomes its own maintenance burden. You tune autovacuum settings. Things stabilize for a while. Data volume increases. The cycle repeats.

Each step is individually correct. The problem is that the sequence never ends. You're working around an architectural mismatch instead of running a workload the architecture was designed to serve.

The engineering cost accumulates in ways that are harder to see on a dashboard. The senior engineer spending a week on partition strategy is not shipping product features. The on-call rotation starts treating "database is slow again" as a recurring incident category. Quarterly planning includes a database scalability line item, every quarter.

## How to Know Which 10% You're In

The answer is already in your table statistics. Not in `EXPLAIN` plans or monitoring dashboards, but in the counters tracking exactly how rows have been written, updated, and cleaned up over the table's lifetime. Run this against your highest-traffic tables:

```SQL
SELECT
    relname AS table_name,
    N
_live_tup,
    n_dead_tup,
    n_tup_ins,
    n_tup_upd,
    ROUND(n_tup_upd::numeric / NULLIF(n_tup_ins, 0) * 100, 2) AS update_pct,
    last_autovacuum,
    last_autoanalyze
FROM pg_stat_user_tables
WHERE schemaname = 'public'
ORDER BY n_tup_ins DESC
LIMIT 10;
```

Here's an example of what a flagged table looks like next to a healthy one:

| table_name | n_tup_ins | n_tup_upd | update_pct | last_autovacuum |
| --- | --- | --- | --- | --- |
| device_metrics | 84,729,3041 | 24,892 | 0.00 | 2025-06-01 14:22:11 |
| user_accounts | 184,203 | 91,843 | 49.86 | 2025-05-29 08:14:03 |

`device_metrics` is in the 10%: 847 million inserts, near-zero updates, and autovacuum fired three minutes ago on a table that has never had a meaningful `UPDATE` run against it. `user_accounts` is not: nearly half its rows are updated, and autovacuum runs only when it actually needs to.

Look for `update_pct` under 5% and `last_autovacuum` timestamps within the last few minutes on tables with near-zero deletes. That's the overhead the [companion piece](https://www.tigerdata.com/blog/postgres-optimization-treadmill) documents in detail: a cleanup process running non-stop on data you never modify, because the storage engine generates that work regardless of your intent.

Pair those numbers against the broader pattern. Your sustained write rate exceeds 10,000 rows per second. Your most common queries aggregate over time ranges, not point lookups by row identifier. You added partitioning specifically to control table size. You upgraded your instance specifically for query performance, not connection headroom.

Three or more of those conditions, and you're in the 10%. The optimization treadmill will keep running, but the trajectory won't change.

## What the 10% Actually Needs

If you've confirmed you're in the 10%, migrating your highest-traffic table starts with a single function call:

```SQL
SELECT create_hypertable('device_metrics', by_range('ts'));
```

This converts the table to a TimescaleDB hypertable, which does automatic time-based chunking without cron jobs or partition management scripts. From there, you can enable columnar storage on your chunks. This format reads only the columns a query requests, not full rows, and compresses historical data by 10 to 20x, bringing time-range aggregation performance in line with what the workload demands. The [migration post](https://www.tigerdata.com/blog/how-to-migrate-your-data-to-timescale) walks through the full process, including zero-downtime options for production tables.

You keep the same SQL, the same connection strings, the same ecosystem tooling. This isn't a replacement for Postgres. It's Postgres with the storage primitives your specific workload actually needs.

## Conclusion

Postgres is not the problem. Running the wrong workload class through an architecture designed for a different problem is. The distinction matters because one has a tuning fix and the other has a structural fix, and those two paths look identical for the first several months.

The most expensive version of this recognition happens after 18 months of optimization effort. The cheapest version happens now.

Run the diagnostic query above. If the numbers land where you expect, read the [full architectural breakdown](https://www.tigerdata.com/blog/postgres-optimization-treadmill). If you're ready to test on your own data, [start a free Tiger Data trial](https://console.cloud.timescale.com/signup) today.