Vitess vs Citus vs CockroachDB: Horizontal Sharding Architecture for MySQL and PostgreSQL (2026)
Every successful relational database eventually hits a wall that a bigger instance cannot fix: write throughput that saturates one primary, a working set that no longer fits in memory, or a recovery time that no longer fits in the business’s patience. At that point teams reach for one of three mature answers. Vitess vs Citus is the MySQL-versus-Postgres version of the question, and CockroachDB is the third path, which abandons the “shard a single-node engine” idea altogether and builds a distributed SQL database from the storage layer up.
These systems look interchangeable in a feature table, yet they make opposite bets about where the shard boundary lives, who owns it, and what happens to a transaction that crosses it. Those bets decide your operational burden for the next five years, long after the benchmark numbers stop mattering.
This guide takes the three apart mechanism by mechanism, with current facts verified against primary sources as of November 2026, and ends with a decision matrix you can defend in a design review.
What this covers: the architecture of each system, how queries get routed, how cross-shard transactions behave, how online resharding really works, the operational and licensing realities, and a decision framework.
Context and Background
Horizontal scaling of relational data has three historical lineages. The first is application-level sharding: your code picks a database by hashing a tenant id. It works, and it is how most large companies started, but it pushes routing, schema migration, and rebalancing into every service. We covered the underlying placement mathematics in database sharding with consistent hashing and rendezvous hashing; this article is about the products that industrialize those ideas.
The second lineage is the middleware sharding layer: a proxy that speaks the database’s wire protocol, hides the shards, and keeps the storage engine unmodified. Vitess is the canonical example. It began at YouTube to survive MySQL load in 2010, joined the Cloud Native Computing Foundation, and graduated there. It remains a MySQL-only system, and the 24.0 release of April 2026 continues to add routing and replication features rather than replace its model (see the Vitess 24 announcement).
The third is the extension approach: keep the engine, patch it from inside. Citus is a PostgreSQL extension that turns a Postgres server into a coordinator and a set of worker nodes. Citus 14.0, announced in February 2026, added PostgreSQL 18 support, and because it is an extension, many PG18 improvements such as asynchronous I/O and skip-scan indexes benefit distributed queries without Citus-specific work (Citus 14 announcement).
The fourth, newer lineage is distributed SQL, inspired by Google Spanner. CockroachDB stores everything in a sorted, replicated key-value map divided into ranges, each range a Raft group. There is no “shard key” you choose at table creation; the database splits and moves ranges on its own. You pay for that automation with a different latency profile and a different cost model.
A useful framing before we go deeper: the three systems answer “who is responsible for data placement?” differently. In Vitess, you declare placement through a VSchema and vindexes, and the system executes your declaration. In Citus, you choose a distribution column per table and the system manages shard placement. In CockroachDB, the system decides placement and you declare only constraints such as locality and survival goals. The further right you go, the less you control and the less you operate, up to the point where the abstraction leaks.
Two orthogonal axes matter throughout. The first is the engine boundary: Vitess and Citus preserve the semantics, extensions, and tooling of MySQL and Postgres respectively, while CockroachDB speaks the PostgreSQL wire protocol but is its own engine with its own gaps. The second is the consistency boundary: single-shard transactions are fast everywhere, while cross-shard transactions range from “two-phase commit if you ask” to “always serializable, always distributed.”
The Three Architectures Side by Side
Direct answer: Vitess puts a stateless routing proxy (vtgate) in front of many ordinary MySQL instances managed by a sidecar (vttablet). Citus makes one Postgres node a coordinator that plans queries over worker nodes holding table shards. CockroachDB has no coordinator at all: every node can accept SQL and the data lives in Raft-replicated ranges that the database splits and rebalances automatically.

Figure 1: Three architectures. Vitess and Citus keep a standard engine per shard and add a routing layer; CockroachDB builds distribution into the storage layer.
The figure shows the structural difference. Vitess and Citus have a deliberate “front door” component, while CockroachDB has a symmetric cluster where any node plays the SQL gateway role for a given connection. In the first two, the engine inside a shard is a real MySQL or PostgreSQL process, with its own storage, write-ahead log, and replication. That is both their greatest strength (every tool you already own still works) and the source of most of their limits (the shard is still a single-writer database).
Vitess: a routing proxy over real MySQL
A Vitess deployment is organized around a keyspace, the logical database. An unsharded keyspace maps to one shard; a sharded keyspace maps to many, with shards named by key-range, for example -80 and 80- for a two-way split of the 64-bit keyspace id space. Each shard has a primary vttablet and replicas, and each vttablet runs beside one mysqld.
The components worth knowing:
- vtgate is the stateless proxy applications connect to. It speaks the MySQL protocol (and gRPC), parses the query, plans it against the VSchema, and sends sub-queries to the right shards. Because it is stateless you scale it like any web tier.
- vttablet wraps a single MySQL instance. It does connection pooling, query rewriting and protection (row limits, query killer), health reporting, and it hosts the VReplication engine and the schema-change machinery.
- The topology service (etcd, ZooKeeper, or Consul) stores the keyspace, shard, and tablet metadata and the VSchema. Losing it does not take down serving for long-lived vtgates, but it stops reconfiguration.
- VTOrc is the failure-detection and repair component, recovering shards when a primary dies. Vitess 24 added ordered recovery execution, which makes semi-synchronous rollouts safer.
- vtctld and the
vtctldclientCLI are the control plane for workflows such asMoveTablesandReshard.
The VSchema is the heart of the model. It declares which tables are sharded, by which column, using which vindex. A vindex is a function from a column value to a keyspace id, and the keyspace id determines the shard. The built-in primary vindexes include hash-style functions for integers and strings; secondary lookup vindexes are backed by a table that maps a non-sharding column (an email address, say) to the keyspace id, so lookups by that column avoid scatter queries.
Citus: Postgres that knows how to be many Postgres
In Citus, you start with a normal PostgreSQL cluster in which every node has the citus extension. One node is the coordinator, and the rest are workers. You call create_distributed_table('events', 'tenant_id') and Citus hash-partitions the table into shards (32 by default, set by citus.shard_count) that live as ordinary Postgres tables on the workers. The coordinator holds only metadata and a “shell” of each table.
When a query arrives, the Citus planner hook intercepts it. If the query filters on the distribution column, the planner routes it to one shard (a “router” query). If it joins co-located tables on the distribution column, it pushes the entire join down and runs it in parallel across shards. Otherwise it falls back to repartitioning or pulling data to the coordinator, which is where performance cliffs live.
Since Citus 11, any node can accept queries once metadata is synced to the workers, which removes the coordinator as a hard throughput bottleneck for read-heavy workloads. Citus 12 added schema-based sharding, where each Postgres schema becomes a unit of placement. That is a natural fit for database-per-tenant applications that never want to pick a distribution column at all.
CockroachDB: ranges and Raft all the way down
CockroachDB stores each table and index as sorted key-value pairs in a single global keyspace. The keyspace is cut into ranges, with a default maximum range size of 512 MiB, after which a range splits. Each range is replicated, three ways by default, through its own Raft consensus group. One replica holds the lease and serves consistent reads and coordinates writes for that range; others follow.
SQL is parsed and planned on whichever node you connect to. That node’s distributed SQL (DistSQL) engine pushes processors toward the nodes holding the data, so aggregation and filtering run next to the ranges. There is no coordinator in the Citus sense, and no router tier in the Vitess sense: the gateway role is per-connection.
The practical difference from the other two is that shard count is not a design decision. A table with ten ranges and one with ten million ranges are the same object to the application. Rebalancing happens continuously: when a node joins, replicas migrate in the background, driven by allocator heuristics. The cost is that every write to a range requires a Raft quorum round trip, and every multi-range write needs a distributed transaction protocol, because the system gives you serializable isolation by default.
Query Routing and Planning
Routing is where the architectures reveal their character, so it deserves a close look. The question for each is: given a SQL statement, how many nodes does it touch, and who decides?

Figure 2: vtgate routes a query with a shard key to one shard, and scatters one without a key, merging results at the gateway.
How vtgate plans
vtgate loads the VSchema from the topology service and caches it. For each statement it builds a plan out of primitives: a Route primitive that sends SQL to a shard set, plus operators for ordering, aggregation, limits, and joins that vtgate evaluates itself when the work cannot be pushed down. The routing opcodes you will see in EXPLAIN FORMAT=vtexplain or the VEXPLAIN statement include EqualUnique (one shard through a unique vindex), IN (a few shards), Scatter (all shards), and Reference or Unsharded for tables that live everywhere or in one place.
Joins between sharded tables are the interesting case. If both tables use the same vindex on the join column, vtgate pushes the whole join to each shard. If not, it performs a nested-loop join at the gateway: it fetches the left side, then issues parameterized queries for the right side per row batch. This works but burns round trips and gateway memory, which is why Vitess schema design is largely an exercise in picking vindexes so that the important joins stay shard-local.
Recent releases keep widening what pushes down. Vitess 24 reports window function pushdown for sharded keyspaces, plus routing rules for views and the ability to target a specific tablet type through USE. None of this changes the design rule: a query whose predicate includes the sharding column on every table is cheap, and everything else is a managed-pain scatter.
A small VSchema illustrates the declaration style:
{
"sharded": true,
"vindexes": {
"hash": { "type": "hash" },
"email_lookup": {
"type": "consistent_lookup_unique",
"params": {
"table": "customer_email_idx",
"from": "email",
"to": "keyspace_id"
},
"owner": "customer"
}
},
"tables": {
"customer": {
"column_vindexes": [
{ "column": "customer_id", "name": "hash" },
{ "column": "email", "name": "email_lookup" }
]
},
"orders": {
"column_vindexes": [ { "column": "customer_id", "name": "hash" } ]
}
}
}
Here customer and orders share the hash vindex on customer_id, so a join on that column is shard-local, and a lookup by email consults the lookup table first and then hits a single shard instead of scattering. The consistent_lookup family keeps the lookup rows consistent with the base table using a pre-write and post-commit protocol so that a crash cannot leave a lookup pointing to the wrong row.
How the Citus planner works
Citus has four planner tiers, tried from cheapest to most expensive: fast-path router (a single-table query with an equality filter on the distribution column), router (multi-table but all filtered to the same co-located shard), logical pushdown (the query can be decomposed so each shard computes a partial result that the coordinator merges), and recursive/logical-planner repartitioning for the hard cases such as joins on non-distribution columns.
Co-location is the lever. Two distributed tables with the same shard count and the same distribution column type are co-located by default, which means shard i of both lives on the same worker. A join on the distribution column is then a purely local join per shard:
-- Distribute both tables on tenant_id so they co-locate
SELECT create_distributed_table('accounts', 'tenant_id');
SELECT create_distributed_table('invoices', 'tenant_id', colocate_with => 'accounts');
-- Small dimension table replicated to every node
SELECT create_reference_table('currencies');
-- Pushed down entirely: join is shard-local, no data crosses the network
SELECT a.name, sum(i.amount)
FROM accounts a
JOIN invoices i ON i.tenant_id = a.tenant_id AND i.account_id = a.id
JOIN currencies c ON c.code = i.currency
WHERE a.tenant_id = 42
GROUP BY a.name;
Reference tables are fully replicated to all nodes, so joining a distributed table to a reference table is always local. They are the Citus answer to small lookup data, and Vitess has an equivalent through “reference” materializations that keep a copy of a table in every shard.
The Citus planner’s weak spot is the same as Vitess’s: a join between two distributed tables on non-distribution columns triggers repartitioning, which shuffles data between workers over the network. You can enable it, but production teams usually treat it as a signal that the schema needs a redesign or that the query belongs in an analytical system.
How CockroachDB routes
CockroachDB’s gateway node consults a range cache, finds the leaseholder for each key span the statement touches, and sends batched KV requests there. A point lookup on the primary key touches one range. A query with no usable index touches every range of the table, which is the same full scan you would see in single-node Postgres, only parallelized by DistSQL.
Because the key order is global, secondary indexes are also distributed independently: an index on email is its own set of ranges, potentially on different nodes than the table rows. A lookup by email is therefore two hops (index range, then primary range), with no vindex declaration needed. This is what a Vitess lookup vindex does manually, but in CockroachDB it is the default behavior of every secondary index.
For latency-sensitive global deployments, CockroachDB adds follower reads (reading slightly stale data from the nearest replica using closed timestamps) and multi-region table localities. REGIONAL BY ROW pins each row to a region via a hidden column, REGIONAL BY TABLE pins a whole table, and GLOBAL tables replicate everywhere with fast local reads at the price of slower writes. Together with survival goals (ZONE or REGION failure), these let you state the topology you want in DDL:
ALTER DATABASE app PRIMARY REGION "us-east1";
ALTER DATABASE app ADD REGION "eu-west1";
ALTER DATABASE app ADD REGION "ap-south1";
ALTER DATABASE app SURVIVE REGION FAILURE;
ALTER TABLE users SET LOCALITY REGIONAL BY ROW;
ALTER TABLE currencies SET LOCALITY GLOBAL;
Vitess and Citus have no counterpart to this declarative geography. Cross-region topologies in both are built from separate clusters, replication, and application routing, which is a legitimate pattern but entirely your engineering.
Cross-Shard Transactions and Consistency
If you remember one section of this article, make it this one. Everyone’s single-shard transaction is fine. The differences appear the moment a write or read spans two shards, and they are the difference between a bug you find in a design review and one you find in a postmortem.
Vitess: choose your guarantee per session
Vitess exposes a transaction_mode with three settings. SINGLE rejects any transaction that touches more than one shard. MULTI, historically the default, lets a transaction span shards but commits them one after another with no atomicity guarantee: if shard A commits and shard B fails, you have a partial write. TWOPC uses a two-phase commit protocol with a durable transaction record so that a crash leaves a recoverable state rather than a torn one.
Two details matter. First, even with 2PC, Vitess does not give you a global snapshot. Each shard is an independent MySQL instance with its own InnoDB MVCC, so two concurrent reads on two shards are not guaranteed to observe the same instant. Cross-shard reads inside a transaction are consistent per shard, not globally. Second, the 2PC implementation has been reworked across recent major versions, so the exact semantics, performance penalty, and maturity depend on the version you run; read the documentation for your release before enabling it in production. I have not benchmarked it here and I do not quote latency numbers for it.
The honest Vitess operating model is therefore: design the schema so that the transactions that must be atomic are single-shard, and treat cross-shard atomicity as an exception handled with 2PC or an application-level saga. That is not a weakness so much as a documented contract, and large Vitess users have lived inside it for years.
Citus: 2PC for writes, no global snapshot
For a distributed transaction that writes to multiple workers, Citus uses two-phase commit, and it records prepared transactions so recovery can finish or abort them after a failure. A transaction that touches a single worker takes the cheap one-phase path. Citus also ships a distributed deadlock detector, because a deadlock can span workers where each node’s local detector sees only half of the cycle.
As in Vitess, there is no cluster-wide snapshot. Citus documents that a multi-shard read may observe a distributed transaction committed on one worker and not yet visible on another, since each worker commits locally in the second phase. For most OLTP and multi-tenant use this is a non-issue because tenant-scoped transactions are single-shard by design, which is the entire premise of the model. For analytical queries over fast-moving data, treat results as approximately consistent.
The practical advice is the same as for Vitess: put the tenant or entity id in every table, make it the distribution column, co-locate, and keep transactions inside one co-location group. Where Citus can be better than Vitess is the Postgres feature surface inside that boundary: full SQL, CTEs, window functions, extensions such as PostGIS and pgvector on the single-shard path, and the whole Postgres tooling ecosystem.
CockroachDB: serializable, distributed, always
CockroachDB takes the opposite stance. Every transaction is serializable by default, whether it touches one range or a hundred. The protocol is MVCC with hybrid-logical-clock timestamps. A transaction writes provisional values called write intents, tracks a transaction record on one range, and commits by flipping that record. An optimization called parallel commits lets the commit overlap with the final batch of writes, so a typical multi-range write costs about one consensus round trip rather than two sequential ones.
What you get is real: no partial commits, no anomalies you must reason about, and snapshot-style reads across ranges. What you pay is latency physics. Every committed write needs a Raft quorum acknowledgment for each range it touches, and in a multi-region cluster with SURVIVE REGION FAILURE, that quorum crosses regions, so write latency is bounded below by inter-region round-trip time. Contended workloads also see transaction retries (error code 40001), which means application code must retry, or use a driver wrapper that does so.
This is the single most important philosophical split among the three. Vitess and Citus say: cross-shard consistency is expensive, so we make you opt in and design around it. CockroachDB says: we pay for it everywhere so you do not have to design around it. Neither is wrong. Which one costs less depends on whether your workload can be partitioned cleanly, and whether you have the engineering discipline to keep it that way as the product evolves.
| Property | Vitess | Citus | CockroachDB |
|---|---|---|---|
| Default isolation across shards | Per-shard InnoDB; no global snapshot | Per-worker Postgres; no global snapshot | Serializable, global |
| Atomic multi-shard writes | Optional 2PC mode | 2PC for multi-worker writes | Always, via transaction records and Raft |
| Cross-shard deadlock handling | Timeouts | Distributed deadlock detector | Contention and retry (40001) |
| Single-shard fast path | Yes, dominant | Yes, dominant | Single-range is cheapest, still Raft |
| App must retry on conflict | Rarely | Rarely | Yes, expected |
Online Resharding and Rebalancing
Choosing a shard layout is a bet on the future, and the future always disagrees. What separates good systems from fragile ones is whether you can change the layout while serving traffic.
Vitess: VReplication, MoveTables, and Reshard
VReplication is the Vitess engine that streams rows between tablets. It first copies a consistent snapshot of the source tables in chunks, then tails the source’s binary log (using GTIDs) to apply changes, and finally keeps the target in near-real-time sync. Everything resharding-related is built from it. The user-facing workflows are MoveTables, which moves tables between keyspaces (for example from an unsharded keyspace into a sharded one, or between clusters), and Reshard, which splits or merges the shards of a keyspace.

Figure 3: The VReplication workflow lifecycle. Reverse replication after the write cutover is what makes the migration reversible.
The flow in the figure maps to concrete commands. A typical split of one shard into two looks like this:
# 1. Create the target shards and start the workflow
vtctldclient Reshard --workflow reshard1 --target-keyspace commerce create \
--source-shards '0' --target-shards '-80,80-'
# 2. Verify target contents match the source
vtctldclient VDiff --workflow reshard1 --target-keyspace commerce create
vtctldclient VDiff --workflow reshard1 --target-keyspace commerce show last
# 3. Move reads first, then writes
vtctldclient Reshard --workflow reshard1 --target-keyspace commerce switchtraffic \
--tablet-types rdonly,replica
vtctldclient Reshard --workflow reshard1 --target-keyspace commerce switchtraffic \
--tablet-types primary
# 4. Roll back if something looks wrong...
vtctldclient Reshard --workflow reshard1 --target-keyspace commerce reversetraffic
# ...or finish and clean up
vtctldclient Reshard --workflow reshard1 --target-keyspace commerce complete
The design has three virtues. VDiff gives you a verified comparison of source and target before any cutover. Staged traffic switching moves replica and rdonly reads before primary writes, so you can observe the new layout under read load first. And when writes are switched, Vitess sets up reverse replication from the new shards back to the old, so reversetraffic is a real rollback with no data loss, not a restore from backup. Vitess 24 added per-shard start and stop for these workflows and automatic retry on other tablets for tablet-specific errors, which addresses a long-standing operational annoyance: a single unhealthy tablet stalling a whole migration.
The same machinery supports far more than splitting. MoveTables is how teams migrate from a standalone MySQL into Vitess in the first place, how they vertically split a hot table family into its own keyspace, and how they move between clouds. The pattern is “create, verify, switch, keep reverse, complete,” and it is the most mature online-migration story of the three.
The cost is operational complexity: copy phases consume I/O on sources, long copy phases accumulate binlog backlog, and the binlog retention window must outlast the longest copy. Large tables also need a primary key or equivalent to copy efficiently.
Citus: shard moves, split, and the rebalancer
Citus resharding comes in two flavors. Rebalancing moves whole shards between workers to even out disk usage or shard counts. citus_rebalance_start() runs it as a background job you can monitor, and citus_rebalance_status() reports progress. Splitting changes the shard count: citus_split_shard_by_split_points can split a shard in two without rewriting the entire table, and isolate_tenant_to_new_shard carves a single noisy tenant into its own shard so it can be moved to dedicated hardware.
Shard moves use PostgreSQL logical replication: copy the shard, stream changes, and switch over with a brief write lock on the shard at the end. Block-moving (older behavior) is no longer the default. Adding a worker follows a simple pattern:
-- Register the new worker, then rebalance in the background
SELECT citus_add_node('worker-3.internal', 5432);
SELECT citus_rebalance_start(rebalance_strategy := 'by_disk_size');
SELECT * FROM citus_rebalance_status();
-- Carve out one heavy tenant onto its own shard, then place it
SELECT isolate_tenant_to_new_shard('events', 4242, 'CASCADE');
Two caveats from experience. Rebalancing only helps if you have enough shards to spread: the default of 32 shards is generous for a handful of workers and tight for dozens, and because the shard count of a co-location group is fixed at creation, picking it too low means a table-rewrite-level effort later. Also, hot tenants defeat even distribution: one customer that is 40 percent of traffic needs isolation, not rebalancing.
CockroachDB: resharding is not an event
In CockroachDB there is nothing to run. Ranges split when they grow past their size threshold or experience heavy load, and replicas rebalance as nodes join or leave. You add nodes, the allocator moves replicas, and you watch the metrics. For most teams this is the decisive advantage: the entire category of “resharding project” disappears from the roadmap.
It does not disappear from the system, though; it moves into the background and must be budgeted. Rebalancing consumes disk and network bandwidth, so rate limits exist for snapshots and recovery. Hot ranges, such as a monotonically increasing key (sequential ids, timestamps as the leading primary-key column), still concentrate load on one range and one leaseholder no matter how many nodes you add. CockroachDB’s guidance is to use UUIDs or hash-sharded indexes for such keys, which is a form of shard-key design smuggled back in through the side door.
Operating Each System
The architecture sets the ceiling for operational burden, and the burden is where the budget goes. I compare them by the work a team does on an ordinary Tuesday and on the worst day of the year.
Day-to-day work
Vitess is the heaviest to assemble. A production deployment includes a topology service, several vtgates, a vttablet and mysqld pair per instance, VTOrc, vtctld, backup storage, and usually the Kubernetes operator (Vitess 24 ships with Operator v2.17.0 and adds cluster-level and keyspace-level backup schedules). You also need people who understand MySQL, replication, and Vitess’s own concepts. In exchange, you get very fine control, and the stack is proven at very large scale at companies that have talked about it publicly.
Citus is the lightest to start. If you already run Postgres, you install an extension and add workers. Managed Citus is also offered on Azure, which removes much of the burden, though you should check the current product name and feature set on Microsoft’s documentation as branding has changed before. The complexity is not in running it but in living with it: schema design, distribution column choice, and avoiding coordinator bottlenecks. Upgrades must be coordinated across the extension and PostgreSQL major versions, and Citus 14’s PG18 support means you plan your Postgres major upgrade around Citus’s release schedule.
CockroachDB is the smallest operational surface for a self-managed team. Nodes are homogeneous; there is no separate topology service or proxy; rolling upgrades are a core feature. The burden shifts to capacity (it replicates three times by default, so raw storage is roughly triple), observability of ranges and contention, and tuning of locality. Most teams also take the managed Cockroach Cloud path because the operational leverage is higher there.
Schema changes and online DDL
Schema evolution is the Tuesday task that breaks the most teams. Vitess includes managed online DDL (strategies such as vitess, gh-ost, and pt-osc) that performs a shadow-table migration per shard, with cut-over coordinated across shards, throttling, and cancel/retry controls. Citus propagates DDL from the coordinator to the workers, and it supports most Postgres DDL, but long-running table rewrites hold locks on each shard and you should use the same techniques you would on stock Postgres (CREATE INDEX CONCURRENTLY, adding nullable columns, backfills in batches). CockroachDB runs schema changes as online background jobs in a state machine that keeps old and new schema versions compatible between nodes, so most ALTER TABLE and CREATE INDEX statements do not block traffic, although they still consume resources and can be disrupted by long transactions.
Backups and disaster recovery
Backups follow the engine. Vitess backs up per tablet using xtrabackup or the built-in engine, and 24.0 adds MySQL CLONE plugin support (it requires MySQL 8.0.17+ and InnoDB-only tables) for faster replica provisioning. Citus is Postgres: base backups and WAL archiving per node, with the challenge of a consistent cluster restore point, which Citus addresses with citus_create_restore_point() to create a named point across all nodes. CockroachDB has built-in distributed backup and restore, including incremental and point-in-time recovery, as part of its core feature set.
The worst day
The comparison gets starker in failure. In Vitess, a primary failing means VTOrc promotes a replica, vtgates reroute, and MySQL semi-sync determines whether you lost transactions; you reason about it as MySQL failover with a routing layer. In Citus, a worker failure is as bad as the worker’s own replication setup: you need physical streaming replicas with a failover tool (Patroni is common) per node, and the coordinator needs the same. In CockroachDB, a node failure triggers lease transfers and Raft re-election within a few seconds, and the cluster continues without operator action as long as a quorum of each range’s replicas survives.
Licensing in 2026
Licensing has become a first-order architecture input, not a legal footnote, so I checked each project’s current position. Please verify against the linked sources before relying on any of it, because license terms change.
Vitess is a CNCF graduated project governed under the Linux Foundation, with the Vitess authors holding copyright, and its source is released under the Apache License 2.0. That is the most permissive of the three and carries no usage restrictions. (I confirmed the Linux Foundation governance on the project site; the Apache 2.0 license is a well-known fact from the repository that I did not re-fetch in this session, so confirm it on GitHub.)
Citus is open source and has been since Citus 11.0, when the formerly proprietary enterprise features (including the non-blocking rebalancer) were folded into the open-source codebase under the AGPLv3. The AGPL is a strong copyleft license: if you offer a modified Citus as a network service you must share your modifications. Using unmodified Citus in your own product is generally unproblematic, but legal teams at vendors who embed or resell database services scrutinize it. The Citus project is operated by Microsoft; the announcement for 14.0 calls it an open-source extension of Postgres, and the license itself is stated in the repository.
CockroachDB is the one that changed. Cockroach Labs retired its self-hosted Core edition on November 18, 2024, coinciding with the 24.3 release, leaving a single Enterprise edition under a source-available license. Enterprise is free for individual developers, students, and companies with under US$10 million in annual revenue (renewable licenses with community-only support), with a 30-day trial for others; the cloud offering was unchanged (SD Times coverage). For larger companies, self-hosting means paying for a license. This is not open source by the Open Source Initiative definition, and organizations with an open-source-only procurement policy cannot adopt it in its current form.
The licensing difference shapes the exit story too. Moving off Vitess means moving to MySQL, which has many managed hosts. Moving off Citus means moving to Postgres, ditto. Moving off CockroachDB means moving to a different SQL dialect with different semantics, and the migration is likely your largest cost.
Choosing: A Decision Framework
Decision matrices are only useful if the axes reflect real pain. Here are the ones I would put in a design review, with a visual flow for the common cases.

Figure 4: A first-pass decision flow. It ranks constraints you cannot change (engine, geography) above preferences (ops style).
| Dimension | Vitess | Citus | CockroachDB |
|---|---|---|---|
| Underlying engine | MySQL | PostgreSQL | Own engine, Postgres wire protocol |
| Who chooses data placement | You, via VSchema and vindexes | You, via distribution columns | System, plus locality rules |
| Resharding | Mature online workflows with rollback | Background rebalancer plus split | Automatic, continuous |
| Cross-shard transactions | Opt-in 2PC, no global snapshot | 2PC on writes, no global snapshot | Serializable by default |
| Multi-region survival | Build it yourself | Build it yourself | Declarative, built in |
| App compatibility | Near-full MySQL, with sharding-aware limits | Near-full Postgres, with distribution rules | Large Postgres subset, retries needed |
| Ops surface (self-managed) | Largest | Medium | Smallest |
| License | Apache 2.0 (verify) | AGPLv3 | Source-available, free under US$10M revenue |
| Best fit | Huge MySQL fleets, hyper-scale OLTP | Multi-tenant SaaS, real-time analytics | Global, correctness-first OLTP |
When Vitess wins
Choose Vitess when you already run MySQL at scale, your data model has a clear sharding key, and you need a mature online migration story with fine control. Its biggest differentiator is that it can absorb an existing application’s MySQL protocol use with limited change, and then grow nearly without bound by adding shards. It is also the most capable at moving data: vertical splits, cross-cluster moves, and large-scale reshards are first-class workflows. Choose it with eyes open about the team: you will need engineers who learn VSchema, vindexes, and the topology service, or a managed provider that carries that load.
When Citus wins
Choose Citus when you are a Postgres shop and your workload is naturally partitioned by tenant, customer, or device. The multi-tenant SaaS pattern, with a tenant_id in every table, is the sweet spot, and schema-based sharding extends it to database-per-tenant designs. Citus also shines for real-time analytics over high-volume append-only data, where parallel pushdown across workers delivers analytical performance within a Postgres interface. It is the lowest-friction path if you want PostgreSQL extensions, familiar SQL, and a small mental-model delta. The risk is the workloads that do not partition cleanly, which will not get better by adding workers.
When CockroachDB wins
Choose CockroachDB when your requirements are about correctness and geography rather than raw single-region throughput: a financial or inventory system that cannot tolerate partial commits, customers in multiple jurisdictions with data residency rules, or a mandate to survive a full region outage without operator action. It is also attractive when you cannot staff a team to run a sharding layer. The costs are write latency tied to quorum distance, application retry logic, a dialect that is close to but not identical to Postgres, and the licensing position described above.
A short note on the “do nothing” option
Before any of these, check whether you need horizontal scaling at all. A modern single-primary Postgres or MySQL on large hardware with read replicas, partitioning, and good indexing handles far more than many teams assume. The honest threshold for sharding is usually write throughput, storage beyond a single volume’s practical limits, or recovery-time objectives, not “we expect to grow.” I have seen sharding projects started a year too early and paid for in permanent complexity.
Trade-offs, Gotchas, and What Goes Wrong
Every one of these systems fails in its own characteristic way. Knowing the pattern ahead of time is the cheapest insurance.
Scatter queries are the silent tax. In Vitess and Citus, a query missing the shard key fans out to all shards. At 8 shards that is tolerable; at 256 it multiplies connection use and tail latency, since the slowest shard determines response time. Instrument the fraction of queries that scatter (Vitess exposes this through query logs and VEXPLAIN; Citus through EXPLAIN and citus_stat_statements) and treat growth as a regression.
The wrong shard key is forever. Changing the sharding column in Vitess means a MoveTables to a new keyspace and a full copy; in Citus, it means a new table and a data migration; in CockroachDB the primary key can be altered, but it triggers a table rewrite. Choose the key by looking at actual query predicates and join paths, not at the entity relationship diagram.
Unique constraints and sequences cross shards badly. A unique constraint across shards requires the unique column to include the shard key (Citus) or a unique lookup vindex (Vitess). Auto-increment ids need a global source: Vitess offers sequence tables, Citus recommends bigserial with care or UUIDs, and CockroachDB steers you to UUIDs because sequential keys create hot ranges.
Hot spots survive scale-out. A celebrity tenant, a viral product, a time-ordered primary key: all concentrate load on one shard or one range. Citus provides tenant isolation; Vitess requires a custom vindex or a reshard with different ranges; CockroachDB offers hash-sharded indexes and range splitting by load. None is free.
Migrations from existing systems are the real project. Application SQL that relied on cross-table transactions, SELECT ... FOR UPDATE across entities, or global sequences will break or degrade. Budget for a test harness that runs your real query log against the target system, using Vitess’s vtexplain, Citus’s EXPLAIN inspection, or a CockroachDB shadow cluster.
Distributed means more things to monitor. Replication lag per shard, 2PC prepared-transaction counts, Raft leader distribution, leaseholder imbalance, and snapshot backlog are different dashboards, and the first time you need them is during an incident.
Cost surprises. CockroachDB’s triple replication multiplies storage, and multi-region survival multiplies cross-region data transfer bills. Vitess runs one replica set per shard, each with its own primary and replicas, so a 16-shard keyspace with three tablets each is 48 MySQL instances. Citus needs HA replicas for the coordinator and each worker. None of these costs appears in the headline throughput chart.
A caution on benchmarks: vendor and community benchmarks for these systems (TPC-C style numbers for CockroachDB, HammerDB or pgbench variants for Citus, sysbench for Vitess) measure different workloads at different scales on different hardware. I have deliberately not included any cross-system throughput numbers, because none that I know of is a fair, independently reproduced comparison. Run your own workload, with your own query mix, before any decision of this size.
Practical Recommendations
Pick the system by the constraint you cannot change. If your engine is locked, the choice is nearly made: MySQL points to Vitess, Postgres with partitionable data points to Citus. If you are free to choose and your hardest requirement is multi-region correctness, CockroachDB deserves the proof of concept. If none of those is binding, run a one-month spike on your top ten queries and let measured scatter ratios and p99 latencies decide.
Whatever you pick, the pre-work is identical, and it matters more than the product:
- Inventory your transactions. List every transaction that touches more than one entity. These are your cross-shard hazards.
- Pick the key from the query log. Rank candidate keys by the share of queries and joins they keep single-shard.
- Prototype with real data volume. Skew, not average size, breaks shard layouts.
- Rehearse the exit and the reshard. Run a full split or rebalance in staging before production needs one.
- Set scatter and retry budgets. Alert when scatter ratio or transaction retry rate exceeds a chosen threshold.
- Check the licence with legal early. AGPL and source-available terms have different consequences for different businesses.
- Plan for managed services. For each system there is a hosted option; price your own operations time honestly against it.
A final piece of opinion, clearly labelled as such: for teams without a dedicated database infrastructure group, the extra capability of Vitess is usually not worth its operational weight unless you are already a large MySQL shop. Citus is the cheapest way to extend Postgres into multi-tenant scale, and CockroachDB is the cheapest way to buy correctness and geography, provided you accept the license and the latency model. For readers also evaluating specialized stores in the same estate, our analysis of time-series database internals and of ClickHouse 26.8 LTS covers cases where a purpose-built engine beats sharding a general-purpose one.
Frequently Asked Questions
What is the main difference in Vitess vs Citus?
Vitess shards MySQL by placing a stateless proxy (vtgate) in front of many MySQL instances, with data placement declared in a VSchema. Citus shards PostgreSQL from inside, as an extension that turns nodes into a coordinator and workers and distributes tables by a chosen column. Both keep a standard engine per shard and have no global snapshot across shards, so the practical choice usually follows your existing database engine and workload shape.
Is Citus vs CockroachDB a fair comparison?
Partly. Both present a Postgres-flavoured SQL interface, but Citus is real PostgreSQL with sharding added, while CockroachDB is a distinct distributed engine. Citus gives you full Postgres features and extensions inside a shard but needs you to design for co-location. CockroachDB gives serializable cross-range transactions and automatic rebalancing but with a Postgres-compatible subset, mandatory retry handling, and a different license model.
Does Vitess support cross-shard transactions?
Yes, in modes you select. SINGLE forbids them, MULTI allows them without atomicity guarantees, and TWOPC runs two-phase commit for atomicity. None provides a global read snapshot, because each shard is an independent MySQL. The 2PC implementation has evolved across versions, so check the documentation for your release before enabling it and test failure scenarios, not just the happy path.
How does Vitess VReplication work for resharding?
VReplication copies a snapshot of the source tables to the target in chunks, then tails the source binary log to apply ongoing changes so the target stays near real time. Workflows like MoveTables and Reshard use it, with VDiff to verify data, staged traffic switching for reads then writes, and reverse replication so you can roll back after cutover. Complete removes the old copy when you are satisfied.
Is CockroachDB still open source?
No, not in the OSI sense. Cockroach Labs retired its self-hosted Core edition on November 18, 2024, leaving one Enterprise edition under a source-available license. It is free for individuals, students, and companies with under US$10 million in annual revenue, with a 30-day trial otherwise. Larger organizations self-hosting need a paid license. Check Cockroach Labs’ current terms before committing, since licensing can change again.
Do I need to shard at all?
Often not yet. A large single-primary PostgreSQL or MySQL with read replicas, partitioning, and sound indexing carries a surprising load. Shard when write throughput, storage beyond a single node’s practical limit, or recovery-time objectives genuinely demand it. Sharding permanently raises complexity in schema design, migrations, and incident response, so the decision should be driven by measured limits and a rehearsed migration plan rather than anticipated growth.
Further Reading
- Database sharding with consistent hashing and rendezvous hashing for the placement mathematics behind these products.
- ClickHouse 26.8 LTS vs 26.3: pipelined SQL and Iceberg for when analytics should leave your OLTP shards.
- Time-series database internals: InfluxDB, Timescale, QuestDB for specialised storage alternatives.
- Announcing Vitess 24, the project’s release notes.
- Distribute PostgreSQL 18 with Citus 14, the Citus announcement.
- CockroachDB retires Core, free Enterprise tier (SD Times) for licensing detail.
By Riju — about
