Agency

Scaling PostgreSQL Horizontally with Citus: A Modern Guide to Distributed SQL at Scale

Citus extends PostgreSQL with distributed database capabilities, allowing data and workloads to be spread across multiple nodes while preserving the PostgreSQL ecosystem developers already know.

LAST UPDATED: April 26, 2026
9 min read
Scaling PostgreSQL Horizontally with Citus: A Modern Guide to Distributed SQL at Scale

PostgreSQL is remarkably capable, but eventually some workloads outgrow the limits of a single database server. When larger datasets, higher write volumes, and increasing query concurrency become the bottleneck, simply adding more CPU or memory stops being enough. Citus extends PostgreSQL with distributed database capabilities, allowing data and workloads to be spread across multiple nodes while preserving the PostgreSQL ecosystem developers already know. The real challenge is not adding nodes—it is choosing the right distribution strategy, keeping related data together, minimizing cross-node queries, and designing an architecture that remains predictable as the system grows.

Why PostgreSQL Eventually Needs Horizontal Scaling

Traditional PostgreSQL scaling usually starts vertically.

PostgreSQL
    │
    ├── More CPU
    ├── More RAM
    ├── Faster Storage
    └── Better Networking

For many applications, this works extremely well.

But eventually a workload can encounter limits around:

CPU

Memory

Storage throughput

Connection capacity

Write volume

Dataset size

Query concurrency

At that point, simply buying a larger database server may become expensive or insufficient.

Horizontal scaling changes the model:

                  Application
                       │
                       ▼
                 Distributed SQL
                       │
          ┌────────────┼────────────┐
          ▼            ▼            ▼
       Worker 1     Worker 2     Worker 3
          │            │            │
          └────────────┼────────────┘
                       ▼
                  Distributed Data

Instead of making one PostgreSQL server increasingly powerful, the workload is distributed across multiple machines.

That is where Citus becomes interesting.

What Citus Changes

Citus extends PostgreSQL with distributed data and query execution capabilities.

A simplified architecture looks like:

Application
    ↓
Coordinator
    ↓
 ┌──┼──────────┐
 ▼  ▼          ▼
W1  W2         W3

The coordinator understands the distributed schema and determines where work needs to happen.

Workers store and process distributed data.

From the application's perspective, the system can still expose a PostgreSQL-compatible interface.

That means teams can continue using familiar technologies such as:

SQL

PostgreSQL drivers

ORMs

Indexes

Transactions where supported by the architecture

Existing PostgreSQL tooling

This is one of Citus's biggest advantages.

You are not abandoning PostgreSQL to adopt a completely different database model.

You are extending it.

Understanding the Citus Architecture

The core architecture has two major roles.

Coordinator

The coordinator receives queries and manages distributed metadata.

Workers

Workers store distributed shards and execute portions of queries.

Conceptually:

                    Client
                      │
                      ▼
                 Coordinator
                      │
             Query Distribution
              /       |       \
             ▼        ▼        ▼
          Worker 1  Worker 2  Worker 3

A table can be distributed into multiple shards:

Orders
 │
 ├── Shard 1 → Worker 1
 ├── Shard 2 → Worker 2
 ├── Shard 3 → Worker 3
 └── Shard 4 → Worker 4

The goal is to allow different parts of the workload to execute in parallel.

But there is an important architectural truth:

Citus scales workloads that can be distributed effectively. It cannot magically parallelize every PostgreSQL query.

Your data model still matters.

Choosing the Right Distribution Column

This is arguably the most important decision in a Citus architecture.

Suppose you have a SaaS platform:

Customers
Orders
Invoices
Events
Users

If each customer represents an independent tenant, `tenant_id` may be a natural distribution key.

For example:

tenant_id = 101
    ↓
Shard A

tenant_id = 202
    ↓
Shard B

tenant_id = 303
    ↓
Shard C

Now queries such as:

SELECT *
FROM orders
WHERE tenant_id = 101;

can be routed efficiently to the relevant shard.

The ideal distribution column generally has:

High cardinality

Good data distribution

Frequent appearance in queries

Stable ownership semantics

Most importantly:

Choose a distribution key based on how the application accesses data—not simply based on which column looks unique.

Suppose your application has:

Tenants
   │
   ├── Users
   ├── Orders
   ├── Payments
   └── Events

If these tables share the same distribution key, related rows can be placed together.

Conceptually:

Tenant 101
   │
   ├── Users
   ├── Orders
   ├── Payments
   └── Events
        ↓
     Same Shard Group

This is called co-location.

It can make joins much more efficient because the database can execute them locally rather than moving large amounts of data between workers.

Compare:

Co-located

Worker 1
 ├── Orders
 └── Users

Local Join

with:

Poorly Distributed

Worker 1 → Orders
     ↓
Network
     ↓
Worker 3 → Users

The second architecture introduces additional coordination and data movement.

This is why distribution strategy should be designed together with the application's query patterns.

Reference Tables and Shared Data

Not every table should necessarily be distributed.

Some data is relatively small and shared across the application.

Examples might include:

Countries

Currencies

Product categories

Configuration

Small lookup tables

These can often be treated as reference data.

Conceptually:

               Reference Data
                    │
       ┌────────────┼────────────┐
       ▼            ▼            ▼
    Worker 1     Worker 2     Worker 3

This avoids forcing every small table into a complex distribution strategy.

The principle is simple:

Distribute large, scalable workloads. Replicate small shared data where that makes architectural sense.

Designing Queries for Distributed Execution

Citus can distribute query execution, but query shape still matters.

Consider:

SELECT *
FROM orders
WHERE tenant_id = 1001;

If `tenant_id` is the distribution key, this can be efficiently routed.

Now consider:

SELECT *
FROM orders
ORDER BY created_at DESC
LIMIT 100;

Across a large distributed dataset, the database may need to coordinate work across multiple workers.

That can require:

Worker 1 ──┐
Worker 2 ──┼──→ Coordinator → Final Result
Worker 3 ──┘

This is not necessarily bad.

But it has different performance characteristics from a query executed entirely on one shard.

When designing distributed SQL, ask:

Can the query be answered locally by one shard?

If yes, you can often achieve excellent performance.

Scaling Writes and Storage

One of the major benefits of distributing a large table is spreading storage and write workload.

Instead of:

One PostgreSQL Server
████████████████████

you can have:

Worker 1  █████
Worker 2  █████
Worker 3  █████
Worker 4  █████

As the dataset grows, additional workers can provide more capacity.

This can be particularly useful for:

Event platforms

SaaS applications

Analytics-heavy transactional systems

High-volume applications

Large multi-tenant datasets

But scaling capacity requires attention to shard distribution.

A poorly selected distribution key can create a hot shard:

Worker 1  ███████████████
Worker 2  ██
Worker 3  ██
Worker 4  █

The cluster has multiple workers.

But the workload behaves like a single overloaded machine.

Distribution quality matters more than node count.

Multi-Tenant Architectures with Citus

Multi-tenancy is one of the most natural use cases for distributed PostgreSQL.

Consider a SaaS platform:

              SaaS Platform
                    │
        ┌───────────┼───────────┐
        ▼           ▼           ▼
      Tenant A    Tenant B    Tenant C
        │           │           │
        ▼           ▼           ▼
      Shards      Shards       Shards

The tenant ID can become the central distribution key.

Then application queries naturally become:

SELECT *
FROM orders
WHERE tenant_id = $1;

This provides a strong architectural alignment between:

Business ownership

Data ownership

Query routing

Scaling

It can also make operational isolation easier to reason about.

For example, a particularly large tenant can be identified as a source of disproportionate workload.

The Importance of Query Locality

A useful concept when designing Citus systems is data locality.

If a request primarily operates on one tenant:

Request
  ↓
tenant_id = 500
  ↓
Single Shard

the architecture can remain highly efficient.

If a request constantly needs:

Tenant A
   +
Tenant B
   +
Tenant C
   +
Tenant D

then distributed coordination becomes unavoidable.

Neither pattern is inherently wrong.

The important thing is to know which one your product actually needs.

Transactions and Distributed Workloads

Transactions become more interesting once data is distributed.

A local transaction on one shard can be much simpler:

Transaction
   ↓
Shard
   ↓
Commit

A transaction involving multiple shards may require distributed coordination.

For example:

Transaction
   ├── Worker 1
   └── Worker 3

That is more expensive and more operationally complex.

This leads to an important design principle:

Design business operations so that the most common transactions stay within a single distribution boundary whenever practical.

For multi-tenant applications, that often means keeping tenant-owned data co-located.

Handling Large Tables and Migrations

Moving from a single PostgreSQL instance to a distributed architecture is not just a database configuration change.

You need to consider:

Table size

Shard count

Distribution key

Existing indexes

Foreign keys

Application query patterns

Downtime requirements

A migration might look like:

Existing PostgreSQL
       ↓
Workload Analysis
       ↓
Choose Distribution Key
       ↓
Create Distributed Tables
       ↓
Move Data
       ↓
Validate
       ↓
Route Application Traffic

Do not select the distribution key after the migration has already started.

Changing a foundational data-placement decision later can be expensive.

Observability and Operational Management

A distributed database creates more things to monitor.

Track:

Coordinator health

Worker health

Shard distribution

Query latency

CPU

Memory

Storage

Network traffic

Cross-node queries

Hot shards

Connection counts

A useful architecture dashboard should answer:

Is the cluster healthy?
        ↓
Is the workload balanced?
        ↓
Are queries staying local?
        ↓
Which shards are hot?
        ↓
Which workers are approaching capacity?

This is more useful than simply monitoring total cluster CPU.

For example:

Cluster CPU = 45%

may look healthy.

But:

Worker 1 = 95%
Worker 2 = 30%
Worker 3 = 25%
Worker 4 = 20%

reveals a distribution problem.

Scaling the Application Alongside Citus

Database scaling and application scaling should be considered together.

A modern architecture might look like:

                    Users
                      │
                      ▼
                 Load Balancer
                      │
            ┌─────────┼─────────┐
            ▼         ▼         ▼
         App 1      App 2      App 3
            │         │         │
            └─────────┼─────────┘
                      ▼
                  Citus
                 /     \
                ▼       ▼
            Worker 1  Worker 2
                │       │
                └───┬───┘
                    ▼
                Worker 3

The application tier can scale horizontally while the database layer distributes storage and query execution.

This creates a platform that can grow in multiple dimensions.

Common Citus Architecture Mistakes

Choosing a Distribution Key Based Only on Uniqueness

A unique ID is not necessarily a good distribution key.

Query patterns matter more.

Ignoring Data Locality

If related data constantly lives on different workers, network coordination can become expensive.

Assuming More Workers Always Means More Performance

A poorly distributed workload can remain bottlenecked by one worker.

Overusing Cross-Shard Queries

Distributed joins and aggregations are sometimes necessary, but excessive cross-shard operations can reduce predictability.

Treating Citus Like a Traditional PostgreSQL Server

The PostgreSQL interface remains familiar, but the execution model is distributed.

Architect accordingly.

Ignoring Tenant Skew

One enterprise customer may generate more traffic than hundreds of smaller tenants.

Plan for uneven workloads.

Making Every Table Distributed

Small shared tables may be better handled as reference data.

Migrating Without Query Analysis

The existing application may contain queries that are poorly suited to distributed execution.

Profile before migrating.

A Practical Distributed PostgreSQL Architecture

A scalable Citus deployment might look like:

                         Applications
                              │
                              ▼
                        Connection Layer
                              │
                              ▼
                         Citus Coordinator
                              │
              ┌───────────────┼───────────────┐
              ▼               ▼               ▼
          Worker 1         Worker 2         Worker 3
          ┌──────┐         ┌──────┐         ┌──────┐
          │Shard │         │Shard │         │Shard │
          │Shard │         │Shard │         │Shard │
          └──────┘         └──────┘         └──────┘
              │               │               │
              └───────────────┼───────────────┘
                              ▼
                     Distributed Dataset

Supporting layers should include:

Monitoring

Backups

Security

Connection management

Capacity planning

Migration tooling

The database should be treated as a distributed platform, not merely a larger PostgreSQL server.

When Citus Is the Right Choice

Citus can be especially attractive for workloads with:

Large datasets

High write volumes

Multi-tenant data

Natural distribution keys

High query concurrency

PostgreSQL compatibility requirements

Horizontally scalable application workloads

It can be particularly compelling for SaaS architectures where tenant boundaries map naturally to data distribution.

For example:

Tenant
  ↓
Distribution Key
  ↓
Shard
  ↓
Worker

This alignment can make the architecture much easier to reason about.

When Citus May Not Be the Best Choice

A distributed database is not automatically better.

Citus may be a poor fit when:

The dataset is comfortably handled by one PostgreSQL server

Queries frequently require global cross-tenant joins

The workload has no natural distribution key

Transactions routinely span many shards

The operational complexity outweighs the scalability requirement

In these cases, improving a single PostgreSQL deployment may be the better engineering decision.

Possible alternatives include:

Vertical scaling

Read replicas

Partitioning

Caching

Query optimization

Connection pooling

Workload separation

Do not distribute a workload simply because distribution is available.

Making the Call

Engineering leaders considering Citus should ask:

What is actually limiting our current PostgreSQL deployment?

Is the problem compute, storage, connections, write throughput, or query concurrency?

Do our tables have a natural distribution key?

Can related data be co-located?

Which queries must remain global?

How frequently do transactions cross data boundaries?

Are some tenants dramatically larger than others?

Can the application consistently include the distribution key in important queries?

Most importantly:

Will distributing the data simplify our scaling problem—or simply move the complexity from one database server into the network?

That is the decision that matters.

Final Takeaway

Scaling PostgreSQL horizontally with Citus is fundamentally a data-modeling and workload-design problem.

The architecture is:

Understand Workload
       ↓
Choose Distribution Key
       ↓
Co-Locate Related Data
       ↓
Design Local Queries
       ↓
Distribute Storage + Compute
       ↓
Monitor Shards
       ↓
Scale Workers

Start with the application's access patterns.

Choose distribution keys around how data is naturally owned and queried.

Keep related tables co-located.

Use reference tables for small shared datasets where appropriate.

Design common transactions to stay within a distribution boundary.

Watch for hot shards and uneven tenant sizes.

Measure cross-node queries rather than assuming the cluster is balanced.

And remember that adding workers is only useful when the workload can actually use them.

The power of Citus is not simply that PostgreSQL can run on more machines. Its real value is that a familiar relational database can be extended into a distributed system while preserving much of the PostgreSQL development experience.

The winning architecture is therefore not:

"Put PostgreSQL on ten servers."

It is:

"Design the data so ten servers can work on the problem efficiently."

When distribution keys, query patterns, tenant boundaries, and operational requirements align, Citus can turn a PostgreSQL system that has reached the limits of vertical scaling into a horizontally scalable distributed SQL platform—without forcing the engineering team to abandon the PostgreSQL ecosystem they already understand.

Frequently Asked Questions

Citus is an extension that transforms PostgreSQL into a distributed database. It scales PostgreSQL horizontally by sharding data and distributing queries across multiple worker nodes, all while maintaining standard PostgreSQL interfaces and capabilities. A coordinator node routes queries to the appropriate workers.
The distribution column determines how data is sharded across worker nodes. A well-chosen column (like a tenant_id in a SaaS app) allows related data to be co-located on the same shard, which ensures fast, local query execution and avoids expensive cross-node joins.
No. Small, frequently joined lookup tables (like countries or currencies) should be created as reference tables, which are replicated to all worker nodes. This prevents unnecessary network traffic and allows them to be joined locally with distributed data.

Need a product built?

We build custom software, mobile apps, and web platforms for startups and enterprises.

Alejandro D.
Vatsalya R.Backend Developer
Gustavo A.
Ganeshan S.Sr. Software Engineer
Fiorella G.
Uptal JoshiSr. Data Scientist

Their team became an extension of ours — within months they'd rebuilt our entire product experience from the ground up.

BitForge
Sr. ArchitectBitForge
Read Case Study