Surviving Multi-Region Database Sharding with CockroachDB

Distributing a relational database across three continents will not make your application faster unless you teach your schema how to respect the speed of light.

VW
Viery Wahyu•Author
•7 min read

Every infrastructure architect eventually dreams of building a database layer that can survive an entire cloud region going dark without dropping a single customer checkout. For years, achieving that level of disaster recovery meant bolting fragile asynchronous read replicas onto a primary PostgreSQL instance and praying that a regional failover would not corrupt un-replicated financial ledger rows. When we migrated our core transactional workloads to a multi-region CockroachDB topology spanning Singapore, Tokyo, and Frankfurt, we expected instant global scalability out of the box. Instead, our very first staging benchmark greeted us with a brutal lesson in network physics when simple single-row inserts suddenly jumped from three milliseconds to over one hundred and sixty milliseconds.

How Raft Consensus Turns Naive Schemas Into Latency Traps

To understand why a multi-region cluster slows down when treated like a single-node database, you have to look at how CockroachDB shards and replicates data under the hood. Every table is automatically sliced into contiguous sixty-four to five-hundred-and-twelve megabyte chunks of sorted key-value data called ranges. Each range is replicated across at least three fault-tolerant nodes using the Raft consensus protocol, with one specific replica elected as the range leaseholder responsible for coordinating all reads and writes for that slice of data.

When we initially deployed our schema without explicit regional locality rules, CockroachDB distributed our range leaseholders evenly across the globe. Whenever a customer in Southeast Asia submitted a booking mutation to our Singapore application gateway, there was a high statistical probability that the leaseholder for their target table range was sitting on a server in Frankfurt. Even worse, before that write could commit under strict serializable isolation, the leaseholder had to broadcast the Raft log entry across undersea fiber cables and wait for a majority quorum of replicas to acknowledge the disk write. No matter how fast your NVMe drives are, packets traveling thousands of kilometers across continents are bound by the speed of light in fiber.

Pinning Data to User Geography With Regional by Row Locality

Eliminating cross-continental round trips on every transaction required aligning our physical range sharding with the actual geographic behavior of our users. A merchant operating a point-of-sale terminal in Bali or a traveler booking a regional tour in Tokyo almost never mutates records belonging to a European branch during the same millisecond. By converting our core operational tables to use REGIONAL BY ROW locality, we instructed CockroachDB to automatically partition ranges along geographic boundaries using a hidden crdb_region column and pin both the leaseholder and the voting Raft quorum inside the user's home region.

sql
1-- Configuring multi-region survival goals and geo-partitioned sharding
2ALTER DATABASE production_core PRIMARY REGION "ap-southeast-1";
3ALTER DATABASE production_core ADD REGION "ap-northeast-1";
4ALTER DATABASE production_core ADD REGION "eu-central-1";
5
6-- Allow the cluster to survive an entire region outage without data loss
7ALTER DATABASE production_core SURVIVE REGION FAILURE;
8
9CREATE TABLE ledger_entries (
10 id UUID NOT NULL DEFAULT gen_random_uuid(),
11 region crdb_internal_region NOT NULL DEFAULT default_to_database_primary_region(),
12 account_id UUID NOT NULL,
13 amount DECIMAL(14, 2) NOT NULL,
14 created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
15 CONSTRAINT "primary" PRIMARY KEY (region ASC, id ASC)
16) LOCALITY REGIONAL BY ROW AS "region";

Look closely at the composite primary key structure on ledger_entries. By placing region as the leading column ahead of the random UUID identifier, CockroachDB physically groups all rows belonging to ap-southeast-1 into dedicated ranges whose leaseholders and local replicas live inside our Singapore availability zones. When a Southeast Asian user reads their account balance, the gateway node serves the query locally in under four milliseconds without ever crossing an ocean. Notice also that we avoided sequential SERIAL integers entirely; in a distributed key-value store, monotonically increasing keys funnel all concurrent inserts into the exact same final range shard, creating a single-node hotspot that ruins horizontal throughput.

Choosing Global Tables for Low-Write Reference Data

While geo-partitioning rows solved our transactional write latency, we still had reference tables like currency exchange rates, platform feature flags, and global catalog categories that every region needed to read in sub-millisecond time. If you leave a reference table pinned to a single primary region, application servers in secondary regions suffer a cross-region network hop every time they join against those lookup rows.

For tables with high read-to-write ratios, we applied the LOCALITY GLOBAL table topology. Under the hood, CockroachDB uses a specialized non-blocking read protocol powered by closed timestamps on global tables. Every region receives a local voting or non-voting replica of the table, allowing application nodes in Singapore, Tokyo, and Frankfurt to execute guaranteed strongly consistent reads straight from their local datacenter disk in one millisecond, trading slightly higher write latency during rare catalog updates for instantaneous global reads.

Embracing Client-Side Retries for Serializable Contention

The final architectural mind shift required to survive a distributed SQL deployment is accepting that transaction retries are a normal part of application life. Unlike standard PostgreSQL, which defaults to a relaxed READ COMMITTED isolation level that permits subtle concurrency anomalies, CockroachDB enforces strict SERIALIZABLE isolation across all nodes. When two concurrent requests in different availability zones attempt to update overlapping index keys within the uncertainty window of physical hardware clocks, the database aborts one of the transactions with standard SQLSTATE code 40001 to guarantee mathematical correctness.

If your backend code treats a 40001 serialization failure like an unrecoverable fatal crash, your users will see random five-hundred errors during traffic spikes. We wrapped our Drizzle database execution layer in a deterministic retry loop with exponential backoff and random jitter so transient clock and lock contentions heal themselves transparently before returning to the caller.

typescript
1import { db } from "@/db"
2import { sql } from "drizzle-orm"
3
4export async function runResilientTransaction<T>(
5 operation: (tx: typeof db) => Promise<T>,
6 maxRetries = 4
7): Promise<T> {
8 let attempt = 0
9
10 while (true) {
11 try {
12 return await db.transaction(async (tx) => {
13 return await operation(tx as unknown as typeof db)
14 })
15 } catch (error: any) {
16 attempt++
17 const isRetryableConflict = error?.code === "40001"
18
19 if (!isRetryableConflict || attempt > maxRetries) {
20 throw error
21 }
22
23 // Exponential backoff with jitter to break distributed lock-step contention
24 const backoffMs = Math.min(1000, Math.pow(2, attempt) * 25 + Math.random() * 40)
25 await new Promise((resolve) => setTimeout(resolve, backoffMs))
26 }
27 }
28}

Building this retry wrapper into our core database module eliminated ninety-nine percent of concurrency exceptions across our financial and inventory pipelines. When two flash-sale checkouts collide on the same stock counter across separate gateway nodes, the losing transaction backs off for forty milliseconds, re-reads the freshly committed inventory count, and completes cleanly on the second pass.

Designing With Physics Instead of Against It

Surviving multi-region sharding with CockroachDB taught us that distributed SQL does not abolish the CAP theorem or accelerate the speed of light through fiber-optic cables. What it gives you is a declarative dial to trade write latency for survivorship on a table-by-table basis. Once you replace sequential IDs with UUIDs, match your table localities to how your users physically interact with data around the globe, and build idempotent retry loops into your transaction boundaries, you get a relational database that shrugs off datacenter blackouts while still feeling fast to users on every continent.

VW

Written by

Viery Wahyu

📍 Bali, Indonesia

Software Engineer

Software engineer based in Bali with over a decade of experience building fast web applications

The Journal

View all →