Siddhant Deval
Siddhant DevalAuthor
Senior Full-Stack Engineer·Jul 26, 2026·18 min read

Cloud-Native & Distributed SQL: CockroachDB and the Horizontal SQL Layer

Distributed SQL solves PostgreSQL's horizontal scaling ceiling — but it introduces cross-region write latency, serializable isolation overhead, and 2PC coordination costs that must be fully understood before committing. This article covers CockroachDB's Raft architecture, multi-region topology, GDPR data residency, and the failure modes that appear reliably at scale.

Cloud-Native & Distributed SQL: CockroachDB and the Horizontal SQL Layer

Selecting the right database is a foundational architectural decision; data gravity ultimately dictates the scalability and resilience of an application — choose the paradigm first, the product second. The broken pattern is reaching for distributed SQL because PostgreSQL "doesn't scale" — without first understanding what "scale" means for your workload. Most teams that hit Postgres's ceiling have a connection pooling problem (solved with PgBouncer), not a sharding problem. Distributed SQL is justified only when you simultaneously need horizontal scale, multi-region high availability, and PostgreSQL wire compatibility. This article shows you what you gain, what you pay, and the failure modes that appear reliably in production CockroachDB deployments.

1. Why PostgreSQL Cannot Shard Horizontally

sql
-- ❌ The broken pattern: manually sharding Postgres by user ID range
-- Shard 1: users 1–1,000,000 → postgres-shard-1.internal
-- Shard 2: users 1,000,001–2,000,000 → postgres-shard-2.internal

-- Application code must now route every query to the correct shard:
const shard = userId <= 1_000_000 ? db_shard1 : db_shard2
const user = await shard.query('SELECT * FROM users WHERE id = $1', [userId])

-- Cross-shard joins are now impossible:
-- SELECT u.name, o.total FROM users u JOIN orders o ON u.id = o.user_id
-- WHERE u.id IN (500000, 1500000)  -- spans two shards — no native JOIN
PostgreSQL's architecture is built on a single Write-Ahead Log (WAL) on a single primary. All writes must flow through the primary — read replicas can offload SELECT traffic but cannot accept writes. This is not a bug; it is a design choice that makes strong consistency and ACID semantics straightforward. The ceiling is a single machine's write throughput.
Manual sharding solves the write throughput problem but creates new ones: cross-shard joins require application-layer stitching, rebalancing shards when data grows unevenly is manual and risky, and a shard failure requires manual failover.
CockroachDB's approach: eliminate the manual sharding layer entirely.

2. CockroachDB Architecture — Raft Consensus Groups

CockroachDB Cluster (3 nodes):

┌─────────────────────────────────────────────────────────────────┐
│ Key Range: /users/1 → /users/999999                             │
│ Raft Group: Node1 (leader), Node2 (follower), Node3 (follower)  │
│ Write: Client → Node1 (leader) → replicates to Node2+Node3      │
│ Commit: once majority (2 of 3) acknowledge → committed          │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Key Range: /users/1000000 → /users/1999999                      │
│ Raft Group: Node2 (leader), Node1 (follower), Node3 (follower)  │
└─────────────────────────────────────────────────────────────────┘
The key insight: data is divided into ranges (default 128MB). Each range has its own Raft consensus group. There is no global single writer — different ranges can be written by different nodes simultaneously. When a node fails, Raft promotes a follower to leader for the affected ranges in seconds, automatically, with no manual intervention.
sql
-- CRDB looks like PostgreSQL to your application:
CREATE TABLE orders (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  user_id UUID NOT NULL,
  total_cents INTEGER NOT NULL,
  created_at TIMESTAMPTZ DEFAULT now()
);

-- Cross-range queries just work — CRDB routes internally:
SELECT u.name, COUNT(o.id) AS order_count
FROM users u JOIN orders o ON u.id = o.user_id
GROUP BY u.name
ORDER BY order_count DESC
LIMIT 10;

3. Serializable Isolation — Correctness vs. Throughput

sql
-- CRDB's default isolation: SERIALIZABLE (stronger than Postgres's READ COMMITTED default)
-- Benefit: no phantom reads, no serialization anomalies — correct by default
-- Cost: higher write contention on hot rows

-- ❌ Broken pattern: not handling serialization retries in the application
try {
  await db.query('BEGIN')
  const balance = await db.query('SELECT balance FROM accounts WHERE id = $1', [id])
  await db.query('UPDATE accounts SET balance = $1 WHERE id = $2', [balance - 100, id])
  await db.query('COMMIT')
} catch (err) {
  await db.query('ROLLBACK')
  throw err  // 40001 SQLSTATE (serialization failure) not retried — silent failure
}

// ✅ Correct: detect 40001 and retry with exponential backoff
async function withRetry<T>(fn: () => Promise<T>, maxRetries = 5): Promise<T> {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await fn()
    } catch (err: any) {
      if (err.code === '40001' && attempt < maxRetries - 1) {
        // Serialization failure — safe to retry
        await sleep(Math.pow(2, attempt) * 50 + Math.random() * 100)
        continue
      }
      throw err
    }
  }
  throw new Error('Max retries exceeded')
}
Performance / Safety Warning
Under high write contention on a narrow key range (e.g., a global counter, a leaderboard top row), CockroachDB's serializable isolation causes a retry storm: many transactions fail with 40001 and must be retried, amplifying write load. Design hot-row access patterns with explicit lock ordering or move high-contention state to Redis (atomic INCR).

4. Data Modeling

4.1 Partition Key Design — Avoiding Range Hotspots

sql
-- ❌ Broken: sequential integer primary key — all inserts go to the same Raft leader
CREATE TABLE events (
  id BIGSERIAL PRIMARY KEY,  -- monotonically increasing: 1, 2, 3, 4...
  -- All inserts hit the highest-value range → single node bottleneck
  payload JSONB
);

-- ✅ Correct: UUID primary key — inserts distributed uniformly across all ranges
CREATE TABLE events (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,  -- random distribution
  payload JSONB,
  created_at TIMESTAMPTZ DEFAULT now()
);

-- For time-series with range queries: hash-prefix sharding
CREATE TABLE metrics (
  shard_id  INTEGER NOT NULL,  -- shard_id = abs(hash(sensor_id)) % NUM_SHARDS
  sensor_id UUID NOT NULL,
  ts        TIMESTAMPTZ NOT NULL,
  value     FLOAT8 NOT NULL,
  PRIMARY KEY (shard_id, sensor_id, ts)
);

4.2 Multi-Region Table Classification

sql
-- REGIONAL BY TABLE: entire table in one region — lowest read latency for that region
-- Use for: user-owned data where all users are in one geography
ALTER TABLE orders SET LOCALITY REGIONAL BY TABLE IN "us-east1";

-- REGIONAL BY ROW: each row in its home region — balanced global access
-- Use for: global user base where each user's data lives in their home region
ALTER TABLE users ADD COLUMN crdb_region crdb_internal_region AS (
  CASE
    WHEN country_code IN ('US', 'CA', 'MX') THEN 'us-east1'
    WHEN country_code IN ('GB', 'DE', 'FR') THEN 'europe-west1'
    ELSE 'asia-northeast1'
  END
) STORED;
ALTER TABLE users SET LOCALITY REGIONAL BY ROW;

-- GLOBAL: replicated to all regions, reads served locally — pay 2× RTT on writes
-- Use for: reference data (country codes, currencies, config) — rarely written
ALTER TABLE currencies SET LOCALITY GLOBAL;
Crucial Requirement
GDPR Compliance: CockroachDB's distributed replication physically copies data across all configured regions. REGIONAL BY ROW is the only configuration that pins EU user data to EU-only regions. Verify your CRDB topology satisfies GDPR Article 44–46 (cross-border data transfers) before production deployment. CRDB Cloud provides explicit region compliance attestations — self-hosted deployments require manual verification.

4.3 Index Design Under Raft

sql
-- ❌ Expensive: secondary indexes in CRDB require cross-range Raft consensus on every write
-- Every INSERT or UPDATE to a indexed column triggers a write to the index range
-- Only create secondary indexes for access patterns that cannot be served by the primary key

-- ✅ Pattern: cover the secondary index to avoid a second lookup
CREATE INDEX idx_orders_user_id ON orders (user_id) STORING (status, created_at, total_cents);
-- STORING columns eliminate the primary key lookup on the index range
-- One Raft round-trip instead of two

5. Multi-Region Write Path

Multi-region CockroachDB write path: client write → Raft leader election → cross-region Raft consensus → commit acknowledgment — annotated with RTT budgets at each step showing where the 2× cross-region overhead originates.
Multi-region CockroachDB write path: client write → Raft leader election → cross-region Raft consensus → commit acknowledgment — annotated with RTT budgets a…
For a REGIONAL BY TABLE table in us-east1, writes from a European client look like:
EU Client → CRDB Gateway (EU) → Raft Leader (US-EAST1)
  RTT 1: EU → US-EAST1 (~100ms transatlantic)

CRDB Raft Leader → Followers (US-EAST1 × 2)
  RTT 2: intra-region replication (~2ms)

Leader commits → Acks EU Gateway → EU Client
  RTT 3: US-EAST1 → EU (~100ms return)

Total write latency: ~200ms P50 for cross-region writes
REGIONAL BY ROW reduces this to intra-region writes for EU users — their rows live in the EU region replica, and the Raft leader for those rows is in the EU cluster.

6. Cost Model at Scale

TierMonthly CostWhen It Fits
CRDB Serverless (0–50M RU/mo)FreeDevelopment, low-traffic staging
CRDB Serverless (paid)~$0.20 per million Request UnitsUnpredictable traffic, < 1M RU/day average
CRDB Dedicated (2 vCPU / 8GB × 3 nodes)~$450/moProduction, predictable workload, > 500K RU/day
CRDB Dedicated + Multi-Region (3 regions × 3 nodes)~$2,700/moGlobal HA, multi-region REGIONAL BY ROW
Self-Hosted (3× c5.2xlarge on EC2)~$300/mo compute + egressOps expertise available, cost-optimized
Architectural Note
Cross-region network egress costs are often the hidden expense in multi-region CRDB deployments. A cluster with 3 regions replicating a 100GB table generates continuous cross-region WAL traffic. Calculate egress costs before committing to a multi-region topology — they can exceed compute costs at large datasets.

7. Security

sql
-- Role-based access control — deny by default, grant explicitly
CREATE ROLE app_service;
GRANT SELECT, INSERT, UPDATE ON TABLE orders TO app_service;
GRANT SELECT ON TABLE users TO app_service;
-- Never grant DELETE to application service roles — use soft deletes (deleted_at TIMESTAMPTZ)

-- Audit logging via CRDB's built-in query audit log
SET CLUSTER SETTING sql.audit.txn.events.enabled = true;
-- Audit log captures: timestamp, user, app_name, statement, latency, error
All CRDB inter-node communication and client connections require TLS — there is no option to disable it in production clusters. Client certificate authentication (mTLS) is supported and recommended for service-to-service connections.

8. When NOT to Use Distributed SQL

ScenarioWhy CRDB FailsBetter Alternative
Single-region deploymentRaft consensus overhead is pure cost with no benefitPostgreSQL + PgBouncer + streaming replicas
Read-heavy workload (> 95% reads)Postgres read replicas serve this at 1/3 the costPostgreSQL + read replicas
Sub-1ms write latency requirementCross-node Raft coordination adds unavoidable ms-level latencyRedis (in-memory), DynamoDB (single-region)
Budget-constrained MVPCRDB Dedicated is 5–10× more expensive than single-node PostgresPostgreSQL on a managed cloud (RDS, Supabase)
No distributed systems expertise on teamClock skew, serialization retries, and compaction require deep familiarityPostgreSQL — failure modes are well-documented and simpler

Summary

ConceptRule
When CRDB is justifiedDistributed SQL is justified only when horizontal scale, multi-region HA, and PostgreSQL wire compatibility are simultaneously required.
Serializable isolation costSerializable isolation is CRDB's default — profile for lock contention before production deployment.
Multi-region write latencyREGIONAL BY ROW reduces cross-region write RTT for geographically partitioned datasets; GLOBAL tables pay 2× RTT on every write.
Wire compatibilityPostgreSQL wire compatibility is ~95% — run integration tests, not just schema diffs, before declaring a migration done.
Clock skewNTP clock skew above 500ms causes cluster instability — this is a hard infrastructure prerequisite, not a recommendation.

What's Next

In Part 5, we cover the specialized data stores — Redis, Elasticsearch, Cassandra, Neo4j, and a beginner-level introduction to vector databases — each solving an access pattern that general-purpose databases serve poorly.
Research & Synthesis Note

This article was developed with AI-assisted deep search, specification cross-referencing, and technical research synthesis.

#CockroachDB#Distributed SQL#Horizontal Scaling#Raft Consensus#Multi-Region#GDPR#CAP Theorem
Siddhant Deval

Written by Siddhant Deval

Senior Full-Stack Engineer building high-scale architectures, browser performance engineering systems, and SaaS platforms.