Deep dive
Service map
| Need | AWS | GCP | Azure | Others |
| Postgres/MySQL | RDS, Aurora | Cloud SQL, AlloyDB | Flexible Server | Neon, Supabase, Crunchy Bridge, PlanetScale (MySQL/Postgres) |
| Key-value / document at any scale | DynamoDB | Firestore, Bigtable | Cosmos DB | MongoDB Atlas, ScyllaDB Cloud |
| Globally consistent SQL | Aurora DSQL | Spanner | Cosmos DB (NoSQL) | CockroachDB, YugabyteDB, TiDB Cloud |
| Analytics warehouse | Redshift, Athena | BigQuery | Synapse, Fabric | Snowflake, ClickHouse Cloud, MotherDuck |
| Time series | Timestream | Bigtable | Data Explorer | TimescaleDB (Tiger), InfluxDB Cloud |
| Edge SQLite | — | — | — | Cloudflare D1, Turso |
Price orders of magnitude (2026-ish)
| Setup | Monthly |
| RDS Postgres db.t4g.micro single-AZ (dev) | order of $15-25 |
| RDS/Cloud SQL 2 vCPU 8 GB, Multi-AZ, 200 GB | order of $250-400 |
| 16 vCPU 128 GB, Multi-AZ, 2 TB, provisioned IOPS | order of $3-6k |
| Aurora storage + I/O (standard) | $0.10/GB-month + $0.20 per million I/Os; I/O-Optimized drops the I/O fee for ~30 % more on instance |
| Spanner | order of $0.9 per node-hour (1000 processing units) regional, plus storage |
| Neon / Supabase small project | free to order of $20-70 |
Decision table
| Situation | Choice | Why |
| New product, relational data | Managed Postgres (RDS, Cloud SQL, Neon, Supabase) | Flexible, every tool supports it |
| Spiky or dev/preview databases | Neon or Aurora Serverless v2 | Branching, scale to zero |
| Simple key access, unpredictable huge scale, AWS shop | DynamoDB | No capacity planning, single-digit ms at any size |
| Multi-region writes with strong consistency | Spanner, CockroachDB, Aurora DSQL | Consensus-replicated ranges, see Raft |
| Read-heavy, global users | Postgres primary + read replicas + cache | Replicas are cheap, consistency stays simple |
| Analytics over app data | Replicate to a warehouse or DuckDB/ClickHouse, don't query the OLTP primary | Analytical scans kill OLTP latency |
| > 10 TB, steady, IOPS heavy | Self-host on NVMe | Managed IOPS pricing |
Code
Postgres settings that matter. Managed services forbid ALTER SYSTEM; set instance-wide values in the parameter group / flags and per-role values in SQL:
-- connection hygiene, per application role (works on RDS, Cloud SQL, Azure)
ALTER ROLE app SET idle_in_transaction_session_timeout = '30s';
ALTER ROLE app SET statement_timeout = '15s';
ALTER ROLE app SET lock_timeout = '5s';
-- parameter group: log_min_duration_statement = 250, shared_preload_libraries = pg_stat_statements
-- visibility
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
SELECT query, calls, mean_exec_time, rows
FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 20;
Pool connections; serverless and many app replicas exhaust max_connections (order of hundreds):
; pgbouncer.ini
[databases]
app = host=mydb.cluster-xyz.eu-central-1.rds.amazonaws.com dbname=app
[pgbouncer]
pool_mode = transaction ; no session state (SET, advisory locks) across statements
max_client_conn = 5000
default_pool_size = 40 ; ~2-4x the DB's vCPUs
server_tls_sslmode = verify-full
DynamoDB single-table access pattern (keys designed for the queries):
# idempotent insert: fails if this order already exists
aws dynamodb put-item --table-name app \
--item '{"pk":{"S":"USER#42"},"sk":{"S":"ORDER#2026-10-20#9001"},"total":{"N":"1999"}}' \
--condition-expression "attribute_not_exists(sk)"
# all October orders of one user: one partition, one sorted range read
aws dynamodb query --table-name app \
--key-condition-expression "pk = :p AND begins_with(sk, :m)" \
--expression-attribute-values '{":p":{"S":"USER#42"},":m":{"S":"ORDER#2026-10"}}'
Efficiency
- Cost: a managed database bills instance hours, storage and I/O. A gp3 volume has a 3,000 IOPS and 125 MiB/s baseline included, up to 80,000 IOPS and 2,000 MiB/s when provisioned, with single-digit ms latency (EBS docs, as of 2026-10), so one default volume caps a database long before the CPU does. PgBouncer's default pool is 20 server connections per user/database pair (PgBouncer docs). Bound: provisioned IOPS and connection count.
- Levers:
- transaction pooling (
pool_mode = transaction) — many client connections share few server connections (PgBouncer docs) — serverless and many app replicas
- provision gp3 IOPS and throughput separately from size — up to 80,000 IOPS and 2,000 MiB/s (EBS docs) — I/O-bound databases
- read replicas plus a cache — reads leave the primary (this page) — read-heavy global users
pg_stat_statements top-20 by total time — fix the few queries that dominate (Code above) — any Postgres
- single-AZ for dev — HA roughly doubles instance cost (Key facts, order of) — non-production
- Naive → standard → state of the art:
- one connection per request on a burstable instance · connection storms, CPU credits run out
- pooled connections, right-sized instance, gp3 with provisioned IOPS · 3,000 IOPS baseline raised on demand
- replicated primary on local NVMe, self-hosted · IOPS bound by the device, not a volume cap (order of; see Self-host vs managed)
Implementation notes
- Test restores: point-in-time restore creates a new instance in minutes to hours depending on size; know the number.
- Read replicas are asynchronous; reading your own write needs the primary or a session-level LSN check.
- Major version upgrades need a plan (blue/green deployments on RDS, logical replication otherwise).
- Encrypt at rest by default and require TLS; put the DB in private subnets only.
- Watch storage autoscaling: it only grows, never shrinks.
Pitfalls
- Burstable instance classes (t-family) run out of CPU credits under steady load.
- gp2/gp3 IOPS limits masquerade as "slow queries"; check
ReadIOPS/DiskQueueDepth.
- DynamoDB hot partitions: one key above order of 1000 writes/s or 3000 reads/s throttles regardless of table capacity.
- Cross-region replicas and backups copy data across regions: billed as transfer.
- Scale-to-zero databases add seconds of latency to the first request after idle.
Implementations
| name | lang | repo | install | verdict | licence | health | as_of | notes |
| Neon | Rust | neondatabase/neon | n/a (service; self-host build per repo docs) | — | — | — | — | serverless Postgres, separated storage |
| PgBouncer | C | pgbouncer/pgbouncer | brew install pgbouncer | — | — | — | — | connection pooler, config in this page |
| sqlx | Rust | github.com/transact-rs/sqlx | cargo add sqlx | — | — | — | — | async Postgres/MySQL/SQLite client |
| pgBackRest | C | pgbackrest/pgbackrest | brew install pgbackrest | — | — | — | — | backup and point-in-time restore |
| Patroni | Python | patroni/patroni | pip install patroni | — | — | — | — | Postgres HA with a consensus store |
Sources
- Alexandre Verbitski et al., Amazon Aurora: Design Considerations for High Throughput Cloud-Native Relational Databases, SIGMOD 2017. paper
- Mostafa Elhemali et al., Amazon DynamoDB: A Scalable, Predictably Performant, and Fully Managed NoSQL Database Service, USENIX ATC 2022. paper
- James C. Corbett et al., Spanner: Google's Globally-Distributed Database, OSDI 2012. paper · ACM TOCS 2013. doi
- AWS, Amazon EBS volume types (General Purpose SSD). docs · PgBouncer, Configuration. docs