Skip to content

Cloud, DevOps & High-load·6 min read

Why We Left AWS Aurora PostgreSQL for Managed Bare Metal to Save 58%

Migrating a high-throughput 12TB PostgreSQL database off AWS Aurora reduced our monthly bill by 58%. Here is the technical breakdown of our storage I/O costs and cutover strategy.

Anatoli NavahrodskiFounder & CEO, GlanitPublished 31 August 2026

The $18.5k Aurora Bill: Storage I/O at Scale

Our 12TB AWS Aurora PostgreSQL database cost surged past $18,500 monthly due to a primary db.r6g.16xlarge instance, two read replicas, and unconstrained I/O billing. Standard Aurora pricing charges $0.20 per 1 million I/O requests. Under a hybrid workload generating 45,000 read/write IOPS, storage I/O alone represented 42% of our monthly invoice. Switching to Aurora I/O-Optimized lowered per-request fees but raised base compute prices by 30%, keeping total monthly spend around $16,200. Moving to managed bare metal with enterprise local NVMe drives eliminated metered I/O pricing entirely, dropping total monthly hardware expense to $7,800.

In October 2023, we spent three weeks attempting to optimize database queries before realizing the log-structured storage architecture itself was the underlying financial problem.

Deconstructing the Aurora I/O Billing Engine

Aurora decouples compute from storage through a proprietary log-structured distributed volume. Every write operation flushes Write-Ahead Logging (WAL) records to six storage nodes spanning three Availability Zones. Every log record write registers as a billable I/O request. In high-throughput transactional systems with frequent index updates, write amplification causes I/O consumption to scale non-linearly against dataset growth.

When analyzing PostgreSQL engine metrics via pg_statio_user_tables alongside CloudWatch data, we identified severe buffer cache miss penalties during nightly batch ingestion. Running db.r6g.16xlarge instances with 512GB of RAM was not enough to prevent working set growth from pulling cache hit ratios below 94%. Those cache misses forced storage reads that immediately billed as read IOPS.

Hardware Benchmarks: Aurora vs. Managed Bare Metal

Managed bare metal replaces Aurora’s virtualized storage tier with dedicated physical servers attached directly to enterprise NVMe drives in RAID 10. Aurora replicates log records over a network fabric across six nodes, which adds write latency under heavy transactional loads. Bare metal instances bypass network-attached storage overhead using PCIe Gen4 direct-attached drives, delivering raw write performance without hypervisor contention.

Deploying physical nodes with AMD EPYC 9654 processors (96 cores, 192 threads, 1.5TB RAM) and enterprise PCIe 4.0 NVMe drives dropped write latency p99 from 4.8ms on Aurora to 0.7ms on bare metal, while raw sequential read throughput increased fourfold.

Architectural and Financial Comparison: AWS Aurora vs. Managed Bare Metal
Metric / Architecture ComponentAWS Aurora PostgreSQL (Standard)AWS Aurora (I/O-Optimized)Managed Bare Metal (RAID 10 NVMe)
Monthly Baseline Cost (12TB DB)$18,500$16,200$7,800
Storage ArchitectureDistributed Log-Structured (EBS-backed network)Distributed Log-Structured (EBS-backed network)Direct Attached PCIe Gen4 NVMe (RAID 10)
I/O Charges$0.20 per 1M Request UnitsIncluded in compute price baseUnmetered (Included in hardware rental)
Write Latency (p99)4.8 ms4.5 ms0.7 ms
Read Throughput LimitConstrained by network interface (up to 40 Gbps)Constrained by network interface (up to 40 Gbps)Local PCIe Bus (~26 GB/sec sequential)
Connection Handling OverheadHigh (Requires RDS Proxy for >5k conn)High (Requires RDS Proxy for >5k conn)Native + Local PgBouncer (Sub-ms routing)
Architectural and Financial Comparison: AWS Aurora vs. Managed Bare Metal

Replacing RDS Proxy with Kernel-Tuned PgBouncer

Replacing AWS RDS Proxy required running PgBouncer instances in transaction-pooling mode on our database infrastructure. RDS Proxy charges $0.015 per vCPU-hour and introduces up to 3ms of latency per transaction hop. Deploying PgBouncer on dedicated bare-metal gateway nodes while tuning Linux kernel socket parameters allowed us to multiplex 15,000 concurrent application connections into 150 backend connections per database instance.

We originally tried running PgBouncer inside a Docker container on the host in transaction mode, but hit severe socket allocation limits under traffic spikes in January 2024.

Kernel Socket Optimization and PgBouncer Configuration

PostgreSQL spawns a separate operating system process per client connection, consuming between 2MB and 10MB of RAM depending on work_mem settings. To handle rapid connection spikes without RDS Proxy, we tuned kernel limits inside /etc/sysctl.conf and locked PgBouncer to transaction mode.

# /etc/sysctl.conf - Network kernel tuning for high-concurrency DB
net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 65535
net.ipv4.tcp_tw_reuse = 1
net.ipv4.ip_local_port_range = 1024 65535
vm.overcommit_memory = 2
vm.overcommit_ratio = 80

The companion pgbouncer.ini configuration routes transient microservice connections into long-lived server pools:

[databases]
app_production = host=127.0.0.1 port=5432 dbname=app_production pool_mode=transaction

[pgbouncer]
listen_addr = *
listen_port = 6432
auth_type = scram-sha-256
auth_file = /etc/pgbouncer/userlist.txt
max_client_conn = 20000
default_pool_size = 150
min_pool_size = 30
reserve_pool_size = 20
max_db_connections = 300
server_idle_timeout = 60

High Availability with Patroni and etcd

Replacing Aurora's automatic storage replication required building a high-availability architecture using Patroni, etcd, and HAProxy across three physical racks. Patroni monitors PostgreSQL streaming replication and executes failovers using raft consensus in etcd when a primary node stalls. HAProxy updates health-check routes to direct incoming writes to the newly promoted primary.

Consensus and Split-Brain Prevention

Aurora relies on a proprietary quorum storage volume to prevent split-brain states. On bare metal, we established a 3-node etcd consensus cluster running on separate management hosts. Patroni interfaces with etcd to maintain leader keys via distributed locks.

  • Primary Node: Renews the leader lock in etcd every 10 seconds.
  • Replica Nodes: Maintain asynchronous physical streaming replication over WAL protocols with hot_standby = on.
  • HAProxy Layer: Polls Patroni's HTTP endpoint (/primary and /replica) on port 8008 to route write queries to the active primary.

If a primary node suffers hardware failure, etcd revokes the leader key. The remaining nodes hold an election, promote the standby with the smallest WAL lag, and notify HAProxy within 8 seconds.

How We Migrated 12TB with Under 4 Minutes of Downtime

Migrating a live 12TB database off AWS with minimal downtime required combining physical base backups via pgBackRest with PostgreSQL logical replication via pgoutput. We provisioned the target cluster, initialized a physical snapshot, configured logical replication slots on the Aurora primary, and streamed Change Data Capture (CDC) events to keep environments in sync. Cutover involved switching Aurora to read-only, applying remaining WAL sequences, verifying sequence offsets, and updating DNS endpoints.

Step-by-Step Migration Sequence

  1. Schema Extraction: Dumped schemas and indexes using pg_dump --schema-only, adjusting parameters for bare metal hardware (such as random_page_cost = 1.1).
  2. Logical Replication Setup: Created a publication on the source Aurora instance for all tables: CREATE PUBLICATION migration_pub FOR ALL TABLES;.
  3. Initial Snapshot: Used parallelized bulk copies via pg_dump and pg_restore with table partitioning to copy baseline state.
  4. CDC Catch-up: Subscribed the bare metal cluster to the publication using CREATE SUBSCRIPTION migration_sub CONNECTION '...' PUBLICATION migration_pub WITH (copy_data = false);.
  5. Sequence Sync and Cutover: Paused application writes, validated table checksums, synchronized auto-increment sequences with custom setval() scripts, and repointed HAProxy endpoints. Total write pause duration was 3 minutes and 42 seconds.

Operational Ownership: NVMe Lifecycles and Backups

Moving from AWS Aurora to managed bare metal shifts responsibility for disk wear monitoring, host hardware maintenance, and point-in-time recovery (PITR) back to engineering. Automated alerting handles disk wear metrics, while continuous WAL archiving via pgBackRest streams to object storage.

In February 2024, our monitoring caught an NVMe drive showing elevated media wear rates after four months of heavy transactional ingestion, allowing us to swap the drive during a routine maintenance window before any read errors occurred.

Enterprise Storage Lifecycles and Backup Strategy

Unlike cloud storage volumes that abstract physical drive health, bare metal NVMe drives have finite write endurance measured in Drive Writes Per Day (DWPD). We configured Prometheus Node Exporter to track SMART parameters—specifically smartctl_nvme_percentage_used and smartctl_nvme_critical_warning—to trigger hardware replacement tickets before failures occur.

For Disaster Recovery, continuous WAL archiving runs via pgBackRest, pushing compressed 16MB WAL segments to S3-compatible object storage every 60 seconds. This setup maintains point-in-time recovery to the millisecond while keeping backup compute overhead isolated from database nodes.

Frequently asked questions