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.

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.
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
| Parameter | Cloud SQL | AlloyDB |
|---|---|---|
| Instance Type | db-custom-16-65536 | 16 vCPU, 128GB RAM |
| Storage | SSD, 10,000 IOPS provisioned | Automatic |
| Region | us-central1 | us-central1 |
| PostgreSQL Version | 15.4 | 15 (AlloyDB-compatible) |
| High Availability | Regional | Regional |
| Read Replicas | 2 | 2 (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:
| Metric | Cloud SQL | AlloyDB | Improvement |
|---|---|---|---|
| Achieved TPS | 8,240 (saturated) | 10,000 (headroom) | 21% |
| P50 Latency | 4.2ms | 2.1ms | 50% |
| P95 Latency | 12.8ms | 5.4ms | 58% |
| P99 Latency | 89ms | 11.2ms | 87% |
| Error Rate | 0.3% (timeouts) | 0.01% | 97% fewer |
| CPU Utilization | 94% | 62% | More headroom |
| Write Latency (P50) | 8.1ms | 3.4ms | 58% |
| Replication Lag | 200-800ms | 20-50ms | 90% 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.
Results: Analytical Query Performance
This is where AlloyDB's columnar engine shines:
| Query Type | Cloud SQL | AlloyDB (Row) | AlloyDB (Columnar) |
|---|---|---|---|
| 24h aggregation | 4,200ms | 3,100ms | 180ms |
| Weekly trend analysis | 12,400ms | 8,900ms | 420ms |
| Full-table percentile | 8,700ms | 6,200ms | 310ms |
| Cross-join reporting | 18,000ms | 14,200ms | 890ms |
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):
| Component | Cloud SQL (scaled up) | AlloyDB | Delta |
|---|---|---|---|
| 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/mo | 0% |
| 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:
- Set up AlloyDB cluster with matching configuration
- Enable logical replication on Cloud SQL source (
wal_level = logical) - Create publication on source for all tables
- Create subscription on AlloyDB target
- Monitor replication lag until caught up (< 1 second)
- Switch application connections — update connection strings
- 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.
Recommended reading

Per-Team Cost Allocation in Shared Kubernetes Clusters: From Chaos to Clarity
Implementing accurate per-namespace cost allocation in multi-tenant Kubernetes clusters, covering request vs. usage attribution, shared resource amortization, and building showback dashboards that drive accountability.

Measuring and Eliminating Toil: From 40% to 12% of Engineering Time
A systematic approach to identifying, measuring, and automating toil—the repetitive operational work that scales linearly with service growth and prevents engineers from doing creative work.

Serverless Postgres in Production: Branching, Scale-to-Zero, and the End of Database Provisioning
Running Neon serverless Postgres in production for 8 months — covering database branching workflows, scale-to-zero economics, connection pooling, and migration from RDS.

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