Back to Blog
Databases & BackendNovember 19, 202511 min read

Scaling Postgres: When Citus, TimescaleDB, and Supavisor Become the Answer

DR

Doug Ribeiro

11 min read

The Adoption Ladder post put multi-region writes, 100M+ vectors, and millions of events per second in Tier 4: do not try. That ceiling is still correct for a single Postgres instance.

This post is about the middle tier that gets skipped in most 'just use Postgres' arguments: the space between 'vanilla Postgres handles it' and 'switch to a distributed system'. There is real daylight between those two, and the default advice ('graduate to a specialized tool') is often premature. Sometimes the right answer is to graduate within the Postgres ecosystem, using extensions and sidecars that solve a specific pain point without giving up everything Postgres got right: SQL, ACID, the tool chain, the hiring pool, the mature backup story.

Four scaling axes cover most of that middle ground. Here is what I would reach for on each, and roughly when.

Connection Scaling: When PgBouncer Is Not Enough

PgBouncer is a Tier 2 requirement in the ladder. For most workloads it is the whole answer. But PgBouncer itself is single-threaded and CPU-bound. Once you are pushing toward ~10,000 client connections and PgBouncer's CPU sits sustained over 70%, you need the next step.

Multiple PgBouncer instances behind a load balancer

Simple, reliable, the textbook answer for another few x of scale. Each app instance talks to a local PgBouncer, and the pool behind them is split across several PgBouncer processes. The operational cost is mostly in making sure pool sizes still add up to something Postgres can actually handle.

Supavisor

Supabase's Elixir-based pooler, built specifically because PgBouncer ran out of runway at Supabase's multi-tenant scale. Multi-tenant, multi-node, cloud-native architecture. Worth looking at if you are running managed Postgres at scale or building a database-as-a-service yourself.

PgCat

A Rust-based alternative to PgBouncer with sharding, failover, and query routing built in. More opinionated than PgBouncer; you get more features, you sign up for more configuration.

When to graduate

PgBouncer CPU sustained over 70%, p99 connection wait time creeping up, or a deployment footprint where running N PgBouncer sidecars per app stack has become an operations smell. If none of those is true, stay on PgBouncer and save the migration for later.

Time-Series Scale: When BRIN and Partitioning Are Not Enough

The ladder recommended BRIN indexes plus declarative partitioning for time-series. That gets you to tens of millions of rows per day comfortably. Beyond that, you want TimescaleDB.

What you get on top of vanilla partitioning:

  • Hypertables. Automatic partitioning with sensible defaults, no manual partition management. You create a hypertable, you insert, it handles the rest.
  • Continuous aggregates. Materialized views that incrementally update as new data arrives instead of doing full refresh. If you are serving dashboards over billions of points, this is the feature that makes it possible.
  • Native compression. 10-20x disk savings on older partitions with minimal query penalty. Critical for anyone keeping more than a few months of high-cardinality data.
  • Retention policies and data tiering as first-class features rather than cron jobs.

The honest catch: TimescaleDB has a dual-license model (in place since 2018, not a recent change despite some confusion on this). The core is Apache 2.0, while the 'Community' tier of features (native compression, continuous aggregates and their refresh policies, retention policies) lives under the Timescale License (TSL). The TSL is permissive for self-hosted use; the one activity it prohibits is offering TimescaleDB itself as a managed database-as-a-service.

Worth a note: the company rebranded from Timescale to Tiger Data in June 2025, which caused some online confusion about whether the license had changed. It did not. Read the license before committing, especially if you redistribute software, but for most teams it is a non-issue.

Graduation threshold: you are keeping more than 30 to 60 days of data on a single Postgres instance, queries against older partitions are measurably slower than queries against recent ones, and you are starting to write custom scripts to manage the partition lifecycle.

Horizontal Sharding: When Vertical Scaling Runs Out

Postgres scales vertically to remarkable sizes. A modern cloud instance gets you 96 vCPU and 768GB of RAM without even trying. For most teams that ceiling is theoretical, and they will never hit it.

For the teams that do, Citus is the answer. Acquired by Microsoft in 2019, the engine behind Azure Cosmos DB for PostgreSQL (which Microsoft is now phasing out for new projects in favor of elastic clusters in Azure Database for PostgreSQL, also built on Citus), and available as an open-source extension you can run on any infrastructure. You declare which tables are distributed (sharded by a chosen key across worker nodes) and which are reference tables (replicated to every worker for cheap joins). Queries get parallelized across workers transparently when the planner can do it.

What Citus will not do: make your schema distributed-friendly for free. You still have to choose a shard key per distributed table, and cross-shard transactions are not free. The best-fit workload is multi-tenant SaaS where you shard by tenant ID, because cross-tenant queries are rare and within-tenant queries stay local to one shard. Complex cross-entity joins across shards are where you feel the constraints.

Graduation threshold: your primary is consistently CPU-bound, you cannot buy a bigger machine or the bigger machine is absurdly expensive, or your write volume has exceeded what a single node's WAL can sustain.

Read Scaling: When One Primary Cannot Serve Your Reads

Streaming replication (physical) gives you hot standbys. It ships the WAL byte-for-byte from primary to replica, so the replica is always an exact copy of the primary at the same Postgres major version. You can route read traffic to them. This is well-trodden territory and supported out of the box since Postgres 9.

Logical replication is the underrated cousin. Instead of streaming the WAL bytes, it decodes the WAL into row-level events (INSERT/UPDATE/DELETE) and ships those to a subscriber. The subscriber can be a different Postgres major version, can subscribe to only some tables, and can have its own indexes. The underlying logical decoding infrastructure has been in Postgres since 9.4, but native publish/subscribe logical replication arrived in Postgres 10 and is what most teams actually use today. Use cases worth knowing:

  • Dedicated analytics replica with different indexes optimized for reporting queries. Your OLTP primary stays lean; your reports run on a replica with aggregate-friendly indexes.
  • Zero-downtime major-version upgrades. Replicate from v15 to v17, verify, cut over. No long maintenance window.
  • CDC without Debezium. Logical replication is already CDC at the Postgres level. For many downstream use cases (search index, data warehouse sync), this is enough.
  • Selective sharding by tenant weight. Hot tenants go to a bigger box via logical replication of only their rows.

Tools worth knowing: native CREATE PUBLICATION / CREATE SUBSCRIPTION, and pglogical for scenarios the native tooling does not cover.

When to Actually Leave

The honest test is whether the ceiling you are hitting is operational or structural.

Operational ceilings stay in the Postgres ecosystem:

  • Running out of connections. Pooler problem.
  • Running out of disk because of partitions. TimescaleDB problem.
  • Running out of CPU on the primary. Citus or vertical scaling problem.
  • Slow reports. Read replica problem.

Structural ceilings mean it is time to add a specialized tool:

  • Sub-millisecond latency under extreme write contention. In-memory stores (Redis, DragonflyDB) exist because memory is not disk.
  • True multi-region active-active writes with strong consistency. CockroachDB, Spanner, and YugabyteDB solve the distributed consensus problem natively. Postgres does not, and stapling it on through third-party tooling trades one set of problems for a worse one.
  • Real analytical workloads on billions of rows. ClickHouse, DuckDB, and Snowflake are genuinely different architectures for a reason. Extensions like pg_duckdb and ParadeDB narrow the gap for smaller OLAP needs, but they do not close it at the top end.
  • Event streaming at Kafka scale. If you genuinely need millions of events per second with replay, partitioning, consumer groups, and fan-out, use Kafka.

The test I use: if you cannot describe the specific extension or sidecar that solves your problem within Postgres, and the ceiling you are hitting is architectural rather than operational, it is time to add a different tool. If you can describe the extension that solves it (and especially if it has a reasonable license and maintained community), stay.

Most teams decide they need a distributed database roughly a year before they actually do. Extending Postgres with the right extension buys back most of that year, usually at a fraction of the cost.

References

PostgreSQLDatabasesScalingArchitecture