AlloyDB vs Cloud SQL for PostgreSQL: Benchmarks at 10K TPS Production Load

Head-to-head performance comparison of AlloyDB and Cloud SQL for PostgreSQL under sustained 10,000 TPS workloads with real-world query patterns.

#gcp#alloydb#postgresql#database
Cover image for the article: AlloyDB vs Cloud SQL for PostgreSQL: Benchmarks at 10K TPS Production Load

AlloyDB promises 4x throughput over standard PostgreSQL and 100x faster analytical queries. Those are marketing numbers. I ran production-realistic benchmarks at 10,000 transactions per second to find out what the actual gains look like — and where AlloyDB's architecture genuinely outperforms Cloud SQL.

The Decision Context

Our platform processes financial transactions requiring strong consistency, complex joins across normalized tables, and mixed OLTP/OLAP workloads. We were hitting Cloud SQL's ceiling at 8,000 TPS on a db-custom-16-65536 instance despite aggressive query optimization.

The options were: vertical scaling to a larger Cloud SQL instance, read replicas with application-level routing, or migrating to AlloyDB. Cost wasn't the primary constraint — latency consistency was.

Architecture Differences That Matter

AlloyDB separates compute from storage, similar to Aurora's architecture but built on PostgreSQL's actual codebase rather than a wire-compatible reimplementation.

AlloyDB vs Cloud SQL Architecture

Key architectural differences:

  • Log-based storage: AlloyDB's storage layer processes WAL records directly, eliminating the write-amplification of traditional PostgreSQL
  • Columnar engine: Automatic columnar caching for analytical queries without manual materialized views
  • Intelligent caching: ML-driven buffer pool management that predicts access patterns
  • Storage scaling: Automatic storage scaling without IOPS provisioning decisions

Benchmark Configuration

Hardware Equivalence

ParameterCloud SQLAlloyDB
Instance Typedb-custom-16-6553616 vCPU, 128GB RAM
StorageSSD, 10,000 IOPS provisionedAutomatic
Regionus-central1us-central1
PostgreSQL Version15.415 (AlloyDB-compatible)
High AvailabilityRegionalRegional
Read Replicas22 (read pool)

Workload Profile

Mixed OLTP/OLAP simulating our production traffic:

  • 60% single-row lookups by primary key
  • 20% multi-table joins (3-5 tables, indexed)
  • 10% range scans with aggregation
  • 5% write transactions (INSERT/UPDATE within transactions)
  • 5% analytical queries (full table scans, GROUP BY, window functions)
-- Representative OLTP query (60% of traffic)
SELECT t.id, t.amount, t.status, t.created_at,
       a.name AS account_name, a.balance
FROM transactions t
JOIN accounts a ON t.account_id = a.id
WHERE t.id = $1;

-- Representative analytical query (5% of traffic)
SELECT DATE_TRUNC('hour', created_at) AS hour,
       category,
       COUNT(*) AS tx_count,
       SUM(amount) AS total_amount,
       AVG(amount) AS avg_amount,
       PERCENTILE_CONT(0.99) WITHIN GROUP (ORDER BY processing_time_ms) AS p99_processing
FROM transactions
WHERE created_at >= NOW() - INTERVAL '24 hours'
GROUP BY 1, 2
ORDER BY 1 DESC, total_amount DESC;

Benchmark Tool

I used pgbench for baseline validation, then a custom Go harness for the mixed workload:

// benchmark_runner.go
package main

import (
	"context"
	"math/rand"
	"sync"
	"sync/atomic"
	"time"

	"github.com/jackc/pgx/v5/pgxpool"
)

type BenchmarkResult struct {
	TotalTransactions int64
	Duration          time.Duration
	Latencies         []time.Duration
	Errors            int64
}

func runMixedWorkload(pool *pgxpool.Pool, targetTPS int, duration time.Duration) *BenchmarkResult {
	ctx, cancel := context.WithTimeout(context.Background(), duration)
	defer cancel()

	result := &BenchmarkResult{}
	var mu sync.Mutex
	ticker := time.NewTicker(time.Second / time.Duration(targetTPS))
	defer ticker.Stop()

	start := time.Now()
	for {
		select {
		case <-ctx.Done():
			result.Duration = time.Since(start)
			return result
		case <-ticker.C:
			go func() {
				queryStart := time.Now()
				var err error

				// Select query type based on workload distribution
				r := rand.Float64()
				switch {
				case r < 0.60:
					err = executeSingleRowLookup(ctx, pool)
				case r < 0.80:
					err = executeMultiTableJoin(ctx, pool)
				case r < 0.90:
					err = executeRangeScan(ctx, pool)
				case r < 0.95:
					err = executeWriteTransaction(ctx, pool)
				default:
					err = executeAnalyticalQuery(ctx, pool)
				}

				latency := time.Since(queryStart)
				mu.Lock()
				result.Latencies = append(result.Latencies, latency)
				mu.Unlock()

				if err != nil {
					atomic.AddInt64(&result.Errors, 1)
				}
				atomic.AddInt64(&result.TotalTransactions, 1)
			}()
		}
	}
}

Results: OLTP Performance

Sustained 10,000 TPS over 30 minutes with warm caches:

MetricCloud SQLAlloyDBImprovement
Achieved TPS8,240 (saturated)10,000 (headroom)21%
P50 Latency4.2ms2.1ms50%
P95 Latency12.8ms5.4ms58%
P99 Latency89ms11.2ms87%
Error Rate0.3% (timeouts)0.01%97% fewer
CPU Utilization94%62%More headroom
Write Latency (P50)8.1ms3.4ms58%
Replication Lag200-800ms20-50ms90% lower

The P99 improvement from 89ms to 11.2ms is the most operationally significant number. Cloud SQL's P99 spikes correlated with checkpoint activity and vacuum operations — AlloyDB's log-structured storage eliminates both bottlenecks.

Latency Distribution Comparison

Results: Analytical Query Performance

This is where AlloyDB's columnar engine shines:

Query TypeCloud SQLAlloyDB (Row)AlloyDB (Columnar)
24h aggregation4,200ms3,100ms180ms
Weekly trend analysis12,400ms8,900ms420ms
Full-table percentile8,700ms6,200ms310ms
Cross-join reporting18,000ms14,200ms890ms

The columnar engine activates automatically for qualifying queries once data is cached. No schema changes, no materialized views, no application changes required.

-- Verify columnar engine usage via EXPLAIN
EXPLAIN (ANALYZE, BUFFERS)
SELECT category, COUNT(*), SUM(amount)
FROM transactions
WHERE created_at >= NOW() - INTERVAL '7 days'
GROUP BY category;

-- Look for "Columnar Scan" in the plan output
-- Columnar Scan on transactions
--   Filter: (created_at >= (now() - '7 days'::interval))
--   Rows Removed by Columnar Filter: 0
--   Columnar cache hit ratio: 99.8%

Cost Comparison

At equivalent performance levels (both running at 10K TPS capacity):

ComponentCloud SQL (scaled up)AlloyDBDelta
Compute$4,800/mo (db-custom-32-131072)$3,200/mo (16 vCPU)-33%
Storage (2TB)$680/mo$520/mo-24%
IOPS$600/mo (provisioned)$0 (included)-100%
Read Replicas (2)$3,200/mo$2,400/mo-25%
Backups$200/mo$200/mo0%
Total$9,480/mo$6,320/mo-33%

AlloyDB costs less at equivalent performance because Cloud SQL requires a much larger instance to achieve the same throughput.

Migration Path

AlloyDB supports native PostgreSQL logical replication, making zero-downtime migration possible:

  1. Set up AlloyDB cluster with matching configuration
  2. Enable logical replication on Cloud SQL source (wal_level = logical)
  3. Create publication on source for all tables
  4. Create subscription on AlloyDB target
  5. Monitor replication lag until caught up (< 1 second)
  6. Switch application connections — update connection strings
  7. Verify data consistency with checksums on critical tables

The entire migration took 4 hours for our 1.8TB database with zero downtime.

When NOT to Use AlloyDB

AlloyDB isn't universally better. Avoid it when:

  • Database size < 100GB — Cloud SQL is simpler and cheaper at small scale
  • Simple CRUD with no analytical queries — The columnar engine won't help
  • Multi-region requirements — AlloyDB is currently single-region (cross-region replicas are in preview)
  • Extreme cost sensitivity — For low-traffic workloads, Cloud SQL's shared-core instances are significantly cheaper

Operational Differences

AlloyDB handles several things automatically that require manual tuning in Cloud SQL:

  • Vacuum: Background garbage collection without table-level locks
  • Checkpoints: Eliminated by log-structured storage
  • Buffer pool sizing: ML-driven automatic tuning
  • Index recommendations: Built-in index advisor based on query patterns

Conclusion

At 10,000 TPS with mixed OLTP/OLAP workloads, AlloyDB delivered 87% lower P99 latency, 23x faster analytical queries, and 33% lower cost compared to an equivalently-performant Cloud SQL deployment. The migration path is straightforward for PostgreSQL workloads.

The decision matrix is simple: if you're running PostgreSQL on GCP at scale where tail latency matters, or you're maintaining materialized views and scheduled aggregations to work around analytical query performance, AlloyDB pays for itself immediately.

Comments

    No comments yet. Be the first to share your thoughts.