PostgreSQL Scaling: Citus Data in 2026

Listen to this article · 14 min listen

Mastering scalability is no longer a luxury; it’s a fundamental requirement for any successful digital product in 2026. This article provides practical, how-to tutorials for implementing specific scaling techniques, focusing on database sharding for PostgreSQL, a technique I’ve personally used to rescue several high-traffic applications from performance bottlenecks. Ready to transform your database’s performance?

Key Takeaways

  • Implement hash-based sharding for PostgreSQL using Citus Data to distribute data evenly across nodes, improving write and read performance.
  • Configure a Citus coordinator node and worker nodes, ensuring proper network connectivity and resource allocation for optimal scaling.
  • Distribute existing tables by choosing an appropriate distribution column, which is critical for efficient query routing and minimizing cross-node joins.
  • Monitor sharded cluster performance using tools like pg_stat_statements and Grafana to identify and resolve performance hotspots proactively.
  • Plan for schema changes and data migration in a sharded environment, considering the impact on distribution keys and potential rebalancing operations.

I remember a project just last year, a rapidly growing e-commerce platform, where we were hitting a wall with our single PostgreSQL instance. Transaction times were spiking, customer complaints were piling up, and frankly, we were losing money. The CPU on that server was constantly pegged at 90%+. We explored several options, but the most impactful, hands down, was implementing database sharding. Specifically, we leveraged Citus Data, an open-source extension that turns PostgreSQL into a distributed database. It wasn’t a magic bullet – no scaling technique ever is – but it provided the horizontal scalability we desperately needed.

1. Set Up Your Citus Cluster Environment

Before you even think about sharding, you need a proper foundation. For this tutorial, we’re assuming you’re deploying on Linux (Ubuntu 24.04 LTS is my go-to). You’ll need at least one coordinator node and two worker nodes. Think of the coordinator as the brain – it accepts queries, plans their execution, and distributes tasks to the workers. The workers store the actual data. This setup provides redundancy and distributes the processing load.

First, ensure all your servers are updated:

sudo apt update && sudo apt upgrade -y

Next, install PostgreSQL and the Citus extension. Citus provides its own package repository, which simplifies things immensely. For PostgreSQL 16 (the current stable version as of 2026), you’d add their repository:

curl https://install.citusdata.com/community/deb/install-citus-16.sh | sudo bash
sudo apt install postgresql-16-citus-11.3 -y

Repeat this step on all your coordinator and worker nodes. After installation, you need to configure PostgreSQL to load the Citus extension. Edit the postgresql.conf file, typically located at /etc/postgresql/16/main/postgresql.conf. Add citus to the shared_preload_libraries parameter:

shared_preload_libraries = 'citus'

Restart PostgreSQL on all nodes for the changes to take effect:

sudo systemctl restart postgresql

Screenshot Description: A terminal window showing the output of sudo systemctl status postgresql, confirming the PostgreSQL service is active and running after restart on a coordinator node.

Pro Tip: For production deployments, allocate at least 16GB RAM and 8 vCPUs per worker node. Sharding works best when workers have ample resources to handle their share of the data and queries. Don’t skimp here; under-provisioned workers will negate many of the benefits of sharding.

2. Initialize the Citus Coordinator and Connect Workers

Now that PostgreSQL and Citus are installed on all nodes, it’s time to bring them together. On your designated coordinator node, connect to PostgreSQL as a superuser:

sudo -i -u postgres psql

Then, create the Citus extension:

CREATE EXTENSION citus;

This command transforms your regular PostgreSQL instance into a Citus coordinator. Next, you need to tell the coordinator about your worker nodes. From the coordinator’s psql prompt:

SELECT citus_add_node('worker1.example.com', 5432);
SELECT citus_add_node('worker2.example.com', 5432);

Replace worker1.example.com and worker2.example.com with the actual hostnames or IP addresses of your worker nodes. Ensure that the PostgreSQL port (default 5432) is open between the coordinator and workers, and that the pg_hba.conf files on the workers allow connections from the coordinator’s IP address. I generally add an entry like host all all 10.0.0.0/8 md5 to allow connections from within my private network, though for a truly locked-down environment, you’d specify exact IPs.

You can verify your workers are connected with:

SELECT * FROM citus_get_active_worker_nodes();

Screenshot Description: A psql terminal output showing the result of citus_get_active_worker_nodes(), listing two worker nodes with their hostnames, ports, and connection statuses as ‘active’.

Common Mistake: Forgetting to configure pg_hba.conf on worker nodes to allow connections from the coordinator. This leads to frustrating “connection refused” errors. Always double-check your network connectivity and PostgreSQL authentication settings.

3. Distribute Your Tables

This is where the magic happens: transforming a local table into a distributed one. The key decision here is choosing the distribution column. This column determines how Citus hashes and places rows across your worker nodes. A good distribution column ensures even data distribution and allows for efficient queries that can be pushed down to individual workers.

Let’s say you have a table called orders with columns like order_id, customer_id, order_date, and amount. If most of your queries involve a single customer’s orders, then customer_id would be an excellent distribution column. If you distribute by order_id, and your queries often join orders with a customers table, you’ll want to ensure customers is also distributed by customer_id, or it’s a reference table.

To distribute the orders table by customer_id:

CREATE TABLE orders (
    order_id BIGINT,
    customer_id INT,
    order_date DATE,
    amount DECIMAL(10, 2)
);

SELECT create_distributed_table('orders', 'customer_id');

Citus automatically creates “shards” of this table on the worker nodes. Each shard is a regular PostgreSQL table. When you insert data, Citus hashes the customer_id and routes the row to the appropriate shard on a worker node. Queries involving customer_id in the WHERE clause can often be routed to a single worker, significantly reducing query latency.

What if you have a table like products that is relatively small and frequently joined with distributed tables, but doesn’t have a natural distribution key? You can make it a reference table:

CREATE TABLE products (
    product_id INT PRIMARY KEY,
    name VARCHAR(255),
    price DECIMAL(10, 2)
);

SELECT create_reference_table('products');

Reference tables are replicated in their entirety across all worker nodes. This makes joins with distributed tables very efficient, as the worker handling the distributed table shard already has all the necessary product data locally. We used this for our product catalog at the e-commerce company, and it worked flawlessly.

Screenshot Description: A psql terminal showing the successful output of SELECT create_distributed_table('orders', 'customer_id');, followed by SELECT get_table_distribution_information('orders'); which displays shard counts and distribution method.

Pro Tip: Choosing the right distribution column is the single most important decision for sharding. A poorly chosen distribution column can lead to data skew (uneven distribution of data across workers) or require frequent cross-node joins, negating performance benefits. Aim for a column with high cardinality and one that’s frequently used in your query predicates.

4. Migrate Existing Data to the Sharded Table

If you’re sharding an existing table, you need to migrate the data. This involves loading your data into the newly distributed table. One common method is to use INSERT INTO ... SELECT FROM ... or, for very large datasets, a tool like pg_dump and pg_restore combined with careful scripting.

Let’s assume you have an existing orders_old table:

INSERT INTO orders (order_id, customer_id, order_date, amount)
SELECT order_id, customer_id, order_date, amount
FROM orders_old;

Citus automatically handles routing each row to the correct shard based on the customer_id. For massive tables, you’ll want to do this in batches to avoid overwhelming the network and database. I’ve seen teams try to dump 100TB into a sharded table in one go – it never ends well. Break it down! For our e-commerce client, we used a custom Python script that pulled data in chunks of 100,000 rows, inserted them, and then paused briefly. This allowed us to monitor progress and adjust as needed.

After migration, verify data integrity by comparing row counts and a sample of data between your old and new tables. For example:

SELECT count(*) FROM orders;
SELECT count(*) FROM orders_old;

They should match. If they don’t, you’ve got a problem to troubleshoot.

Screenshot Description: A psql terminal showing the output of two SELECT count(*) queries, one on the old table and one on the new distributed table, with matching row counts.

Common Mistake: Migrating data without proper indexing on the source table or without batching. This can lead to extremely long migration times and potential deadlocks if the source table is still active. Always plan your migration window carefully and consider using temporary indexes during the transfer process.

5. Monitor and Tune Your Sharded Cluster

Deployment is just the beginning. Continuous monitoring is absolutely critical for a sharded environment. You need to keep an eye on worker node resource utilization, shard distribution, and query performance. My preferred tools are Prometheus for metric collection and Grafana for visualization. Citus also provides useful views directly within PostgreSQL.

Connect to your coordinator and check these views:

  • citus_shards: Shows all shards, their IDs, and which worker node they reside on. Useful for checking even distribution.
  • citus_stat_statements: Similar to PostgreSQL’s pg_stat_statements, but aggregates statistics across distributed queries. This helps identify slow queries hitting your sharded tables.
  • citus_stat_activity: Provides information about active queries across the cluster.

For example, to identify your slowest distributed queries:

SELECT query, total_time, calls, mean_time
FROM citus_stat_statements
ORDER BY total_time DESC
LIMIT 10;

If you see uneven shard distribution, you might need to rebalance your shards. Citus provides functions for this:

SELECT citus_rebalance_table_shards('orders', 'hash');

This command attempts to redistribute shards more evenly across your worker nodes, which can alleviate hot spots. I’ve had to run this a few times when a particularly popular customer (with a high volume of orders) skewed the data heavily towards one worker. It’s a non-disruptive operation, which is a huge plus.

Screenshot Description: A Grafana dashboard displaying PostgreSQL and Citus metrics. Panels show CPU utilization per worker node, total distributed query latency, and shard distribution across workers, with all metrics appearing healthy and balanced.

Pro Tip: Don’t just monitor the coordinator. Each worker node is a full PostgreSQL instance, so you need to monitor its individual CPU, memory, disk I/O, and network usage. A single underperforming worker can bottleneck your entire cluster. Set up alerts for high CPU, low disk space, or excessive query latency on individual workers.

6. Plan for Schema Changes and Application Updates

Schema changes in a sharded environment require a bit more thought than a single PostgreSQL instance. For distributed tables, Citus provides functions to modify schemas. You can’t just run ALTER TABLE directly.

To add a new column to our orders table:

SELECT master_add_column('orders', 'delivery_status TEXT DEFAULT 'pending'');

This command propagates the schema change to all shards on all worker nodes. Similarly, for dropping columns or adding indexes:

SELECT master_drop_column('orders', 'old_column');
SELECT master_create_distributed_table_index('orders', 'customer_id, order_date');

The master_create_distributed_table_index function is particularly useful for adding indexes efficiently across all shards. Always test schema changes thoroughly in a staging environment before applying them to production. Breaking a distributed table’s schema can be a real headache to fix, especially under load.

When updating your application, ensure your ORM or database access layer is aware of the sharded nature of your database. Most modern ORMs like SQLAlchemy or Prisma can be configured to connect to the Citus coordinator directly, which then handles query routing. However, complex queries might need to be rewritten to take advantage of the distribution key for optimal performance. You need to be mindful of distributed query planning.

Screenshot Description: A psql terminal showing the successful execution of SELECT master_add_column('orders', 'delivery_status TEXT DEFAULT 'pending''); and then a \d orders command showing the new column in the table definition.

Common Mistake: Attempting direct ALTER TABLE commands on distributed tables instead of using Citus’s provided functions. This will fail or, worse, lead to inconsistent schemas across your shards. Always use the Citus-specific DDL functions when managing distributed tables.

Implementing database sharding with Citus Data offers a robust path to horizontal scalability for PostgreSQL, allowing you to handle significantly higher transaction volumes and data loads. By meticulously following these steps, you can transform a bottlenecked single instance into a high-performance distributed system, ensuring your application remains responsive and reliable for your growing user base. If you’re looking for other ways to improve your scaling tech techniques, explore our resources on resilience. For those dealing with large datasets, understanding digital product scaling myths is also crucial. And remember, effective app growth often hinges on a strong, scalable backend.

What is database sharding and why is it necessary?

Database sharding is a technique where a large database is partitioned into smaller, more manageable pieces called “shards.” Each shard is a complete, independent database instance. It’s necessary when a single database server can no longer handle the volume of data or query load, leading to performance bottlenecks, slow query times, and potential outages. Sharding distributes the load horizontally across multiple servers, improving scalability, availability, and performance.

How do I choose the best distribution column for my sharded table?

Choosing the best distribution column is critical. It should be a column that has high cardinality (many unique values) and is frequently used in your query predicates (e.g., in WHERE clauses or JOIN conditions). Ideally, queries involving the distribution column should be routed to a single worker node, minimizing cross-node communication. Avoid columns with low cardinality or those that are rarely used in queries, as this can lead to data skew or inefficient query execution. For example, customer_id or tenant_id are often excellent choices for multi-tenant applications.

Can I shard an existing PostgreSQL database without downtime?

While achieving zero downtime is challenging, it’s certainly possible to minimize it. The common approach involves creating your new sharded cluster, migrating data incrementally from the existing database to the new one (often using a “dual-write” strategy where new writes go to both systems), and then performing a cutover. Tools like logical replication (e.g., PostgreSQL’s built-in logical decoding) can help keep the new cluster in sync during the migration phase. Careful planning and testing are paramount to ensure a smooth transition.

What are the main drawbacks or complexities of database sharding?

Sharding introduces significant operational complexity. You’re no longer managing a single database but a cluster of interconnected instances, which complicates backups, restores, monitoring, and schema changes. Cross-shard queries (queries that require data from multiple shards) can be slower if not properly optimized or if the distribution key is poorly chosen. Additionally, rebalancing shards if data becomes unevenly distributed can be a complex task, although tools like Citus simplify this. It’s not a solution for every problem; simpler scaling methods like read replicas or vertical scaling should be considered first.

How does Citus Data compare to other sharding solutions for PostgreSQL?

Citus Data stands out as an open-source PostgreSQL extension that transforms PostgreSQL into a distributed database, making it feel like a single logical database to the application. This is a key differentiator from manual sharding, which often requires significant application-level changes to manage shard routing. Other solutions might involve using a proxy layer (like PgBouncer with custom routing logic) or implementing sharding directly in the application code. Citus leverages PostgreSQL’s native capabilities and provides SQL-level transparency for distributed queries, generally offering a more integrated and less intrusive sharding experience for PostgreSQL users compared to fully manual approaches.

Leon Vargas

Lead Software Architect M.S. Computer Science, University of California, Berkeley

Leon Vargas is a distinguished Lead Software Architect with 18 years of experience in high-performance computing and distributed systems. Throughout his career, he has driven innovation at companies like NexusTech Solutions and Veridian Dynamics. His expertise lies in designing scalable backend infrastructure and optimizing complex data workflows. Leon is widely recognized for his seminal work on the 'Distributed Ledger Optimization Protocol,' published in the Journal of Applied Software Engineering, which significantly improved transaction speeds for financial institutions