How Amazon Aurora Works Internally: The Log-Is-the-Database Architecture, Quorum Writes, and Storage-Compute Separation Behind Cloud-Native SQL
A deep dive into Aurora's storage-compute separation, the log-is-the-database design that pushes redo log processing to storage nodes, quorum-based replication across 6 copies in 3 AZs, protection group architecture, fast cloning, Serverless v2 scaling mechanics, and honest tradeoffs versus RDS, self-managed PostgreSQL, CockroachDB, and Cloud Spanner.
Most managed databases are just a self-managed database with an operator wrapped around it. Aurora is not that. AWS re-architected the storage layer from scratch, and the result behaves differently enough from standard PostgreSQL or MySQL that the operational mental model you bring from those systems will mislead you if you don’t understand what changed.
This article covers how Aurora actually works: the storage-compute split, the “log is the database” design, quorum writes, protection groups, cloning, Serverless v2 autoscaling, and read replica replication. Everything that makes Aurora behave differently from what you’re used to.
The Core Problem Aurora Solves
Traditional databases write two things on every transaction commit: the write-ahead log (WAL) record for durability, and the modified data pages to the buffer pool. When the buffer pool flushes dirty pages to disk, those writes go to storage too. In a replicated setup you’re sending both WAL and full pages across the network to standbys.
Aurora’s team observed that in a cloud environment the bottleneck isn’t CPU or memory on the database instance. It’s the network I/O between the compute layer and storage, and between the primary and its replicas. Their solution was to collapse the write path down to a single thing: the redo log.
Storage-Compute Separation
In Aurora, the compute layer (the database instance running PostgreSQL or MySQL-compatible code) is completely separate from the storage layer (a purpose-built distributed storage system managed by AWS). The two communicate over a low-latency network.
The database instance holds the buffer pool in memory exactly as PostgreSQL does. Reads that miss the buffer pool go to the Aurora storage layer. Writes never touch a disk local to the compute instance at all. The instance writes redo log records across the network to the distributed storage layer, and that’s the only I/O path for writes.
This separation has a few immediate consequences:
- Failover is fast because the new primary doesn’t need to replay a WAL from a local disk. The storage layer already has everything. A cold compute instance can open the shared storage volume and start serving within seconds.
- Read replicas share the same storage volume as the primary. They don’t receive WAL shipped over the network and apply it locally. They receive log records (much smaller than full pages) and use them to keep their buffer pools current.
- Storage scales independently of compute. You don’t resize the instance to get more IOPS.
The Log-Is-the-Database Design
This is the architectural insight Aurora is built on. In a traditional database, the WAL is a means to reconstruct pages. The page is the authoritative representation of data on disk. Aurora inverts this: the redo log stream is the authoritative representation. Pages are a materialized cache of the log.
When the Aurora compute instance commits a transaction, it sends the redo log records across the network to the storage layer. It does not send modified pages. The storage nodes receive those records and apply them to reconstruct pages on demand. The storage layer is responsible for materializing pages from the log, not the database instance.
This is why Aurora’s write I/O is so much smaller than traditional MySQL or PostgreSQL under the same workload. A single page write in InnoDB generates I/O proportional to the page size (16KB by default). An equivalent Aurora write generates I/O proportional to the redo log record, which is typically a fraction of that.
The practical effect: Aurora sustains higher write throughput with less I/O pressure than an equivalent RDS instance running the same engine.
Quorum Replication Across Six Copies in Three AZs
Aurora stores six copies of every piece of data across three Availability Zones: two copies per AZ. This is not traditional synchronous replication where the primary waits for all standbys to acknowledge. Aurora uses a quorum model.
Write quorum: 4 of 6 copies must acknowledge before a write is committed.
Read quorum: 3 of 6 copies must respond consistently before a read is satisfied.
The quorum math matters because it defines the failure tolerance. With a 4/6 write quorum:
- Aurora can absorb the loss of an entire AZ (losing 2 copies) and still commit writes, because 4 copies remain in the other two AZs.
- Aurora can absorb the loss of one additional copy from a second AZ (total 3 copies lost) and still commit writes, because 3 remaining copies still satisfy a read quorum, keeping data accessible even if writes are paused.
In practice, Aurora achieves durability guarantees well above those of a single-region RDS Multi-AZ deployment while completing writes faster, because 4/6 is faster to satisfy than waiting for a synchronous standby in a different AZ to fully apply all changes and flush to disk.
The quorum responses also carry metadata that allows the storage layer to detect and repair inconsistent copies in the background. Storage nodes gossip among themselves to identify which copies are behind and initiate peer-to-peer repair without involving the compute layer.
Protection Groups and the 10GB Segment Architecture
The Aurora storage volume is not a monolithic slab. It is divided into protection groups, each covering a 10GB segment of the logical volume.
Each protection group has its own six-copy quorum across three AZs. This means:
- The storage volume for a 500GB database consists of 50 protection groups, each with 6 copies distributed across 3 AZs.
- A storage node failure only affects the protection groups that node hosts. Peer-to-peer repair for those 10GB segments happens independently of the rest of the volume.
- Repair bandwidth is bounded per segment: rebuilding a 10GB protection group from peer nodes is fast (seconds to minutes under normal network conditions) compared to rebuilding a full database volume.
- Write and read quorums are satisfied per-segment, so a slow or degraded storage node only affects the segments it participates in.
From the compute layer’s perspective, the storage volume is a single logical block device. The protection group segmentation is invisible to the database instance. Internally, however, the storage service routes each write to the appropriate 6-copy protection group based on the logical byte offset.
Fast Database Cloning
Aurora cloning uses the protection group architecture to create a copy-on-write snapshot of the storage volume without copying data.
When you clone an Aurora cluster, the new cluster shares the same underlying protection groups as the source. Both clusters read from the same physical pages. The storage layer tracks which pages have been modified by each cluster since the clone was created. When the source or the clone writes to a page, the storage layer performs a copy-on-write: the original page stays associated with whichever cluster last owned it, and a new page is allocated for the cluster making the modification.
The result is that a clone of a 5TB Aurora cluster is created in seconds and initially costs no additional storage. Storage diverges over time as each cluster accumulates writes post-clone. This is operationally useful for production database copies for staging environments, schema migration testing, or data analysis without impacting the production cluster.
The TypeScript code connecting to a cloned Aurora cluster is identical to connecting to the original:
import { Pool } from "pg";
// Clone has its own endpoint but shares underlying storage until divergence
const stagingPool = new Pool({
host: process.env.AURORA_CLONE_ENDPOINT,
port: 5432,
database: "myapp",
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
ssl: { rejectUnauthorized: true },
max: 20,
idleTimeoutMillis: 30_000,
});
Point-in-time recovery (PITR) uses the same mechanism. Aurora continuously backs up the redo log stream to S3 with second-level granularity. Restoring to a specific point creates a new cluster from the most recent snapshot plus the intervening log records. Because the storage layer materializes pages from logs, there is no separate “apply recovery” phase visible from the outside. The new cluster is simply available at the target timestamp.
Read Replica Architecture and Sub-20ms Replication Lag
Aurora read replicas share the same storage volume as the primary. They do not maintain a separate copy of the data. What differs between the primary and a replica is the buffer pool state.
When the primary writes redo log records to the storage layer, those records are also sent directly to connected read replicas via a dedicated replication path. The replicas use these records to invalidate or update their buffer pool entries. This means replicas do not need to re-read data from storage for every invalidation; they can apply the change directly to the in-memory page if it’s present.
The replication lag in Aurora is typically under 20ms under normal load because:
- The replica receives log records from the primary’s commit path, not from a separate log shipping process.
- The replica does not need to write anything to its own storage (it shares storage with the primary).
- The only work the replica does is update its buffer pool state.
This contrasts with PostgreSQL streaming replication where the standby receives WAL, writes it to its own WAL buffer, applies it to its buffer pool pages, and flushes pages to its local disk periodically. Each of those steps adds latency.
Routing read traffic to replicas is handled via the Aurora reader endpoint, which load-balances across all replicas in the cluster. A common pattern is to direct heavy analytical queries or reporting workloads to replicas to keep the primary focused on write-critical transactions.
// Writer endpoint for transactions that write
const writerPool = new Pool({
host: process.env.AURORA_WRITER_ENDPOINT,
max: 10,
// ...
});
// Reader endpoint load-balances across replicas for read-heavy queries
const readerPool = new Pool({
host: process.env.AURORA_READER_ENDPOINT,
max: 30,
// ...
});
async function getProductCatalog(filters: ProductFilters) {
// Safe to route to reader: no writes, tolerates <20ms stale data
const { rows } = await readerPool.query(
`SELECT id, name, price, inventory_count
FROM products
WHERE category = $1 AND active = true
ORDER BY name
LIMIT $2 OFFSET $3`,
[filters.category, filters.limit, filters.offset]
);
return rows;
}
Aurora Serverless v2 Scaling Mechanics
Aurora Serverless v2 adds autoscaling to the compute layer. The storage layer is always the same distributed system; what changes is how much compute capacity is allocated to the database instance.
Capacity is measured in ACUs (Aurora Capacity Units). One ACU is approximately 2GB of memory and proportional CPU. You configure a minimum and maximum ACU range, and Aurora scales within that range based on observed load.
The scaling mechanism works by monitoring CPU utilization, database connections, and internal queue depth. When utilization crosses thresholds, Aurora adds ACUs in fine-grained increments (as small as 0.5 ACU) rather than doubling instance size. This makes scaling faster and less disruptive than v1’s approach of suspending and resuming the cluster.
Scaling does not cause a failover or connection reset because the underlying storage does not change. The compute instance gets more or fewer CPU and memory resources while maintaining all open connections. The buffer pool content is preserved during scale-up; during scale-down, Aurora evicts buffer pool pages to reduce memory footprint before reducing ACU allocation.
The minimum ACU can be set to 0 for truly intermittent workloads (the database suspends to zero when idle and resumes on connection, accepting ~30 seconds of cold-start latency). For production APIs with consistent traffic, a non-zero minimum eliminates cold starts.
// Environment-aware pool sizing for Serverless v2
// ACU scales automatically; size the pool based on expected concurrency,
// not instance class, since there is no fixed instance class
const pool = new Pool({
host: process.env.AURORA_WRITER_ENDPOINT,
max: process.env.NODE_ENV === "production" ? 50 : 5,
idleTimeoutMillis: 60_000, // release idle connections; v2 scales down
connectionTimeoutMillis: 5_000,
});
One subtlety: because Aurora Serverless v2 scales compute (not storage), it cannot escape storage-layer constraints. A query that does a full sequential scan of a large table will generate the same storage I/O regardless of ACU count. Autoscaling CPU does not help if the bottleneck is storage read throughput.
Production Considerations
Connection management. Aurora does not include a built-in connection pooler. At scale, the number of database connections becomes a bottleneck before CPU or memory does. Use RDS Proxy (which pools connections between your application and Aurora) or a dedicated PgBouncer layer. Each idle connection in PostgreSQL-compatible Aurora consumes memory on the compute instance.
Replica lag under heavy write load. Sub-20ms replication lag is the common case. Under sustained heavy writes (bulk loads, large transactions), replica lag can increase to seconds. Design read workloads that can tolerate this, or use the writer endpoint for reads that need strict consistency.
Storage auto-scaling and costs. Aurora storage grows automatically in 10GB increments but does not shrink. If you delete rows, storage usage does not decrease until Aurora performs internal background cleanup (equivalent to VACUUM in PostgreSQL). Monitor VolumeBytesUsed over time and factor in that a large delete followed by a re-insert can temporarily double storage consumption.
Parameter groups and engine version pinning. Aurora maintains its own minor versions that may lag behind open-source PostgreSQL or MySQL releases. Verify extension availability (especially for PostgreSQL) before migrating workloads that depend on specific extensions. Some extensions available in self-managed PostgreSQL are not available in Aurora PostgreSQL.
PITR retention and backup costs. PITR is enabled by default with a retention period of 1 to 35 days. Longer retention means more log storage on S3. At high write rates, the redo log stream to S3 can accumulate significant storage costs independent of your data volume.
Monitoring quorum health. CloudWatch exposes AuroraStorageHealthyNodes per AZ. If this metric drops below 2 for any AZ, you are approaching the minimum node count for quorum satisfaction. Set alarms on this metric, not just on CPU and latency.
Failover duration. Aurora promotes a replica to primary in approximately 30 seconds (often less). During this window, write connections will receive errors. Applications must handle connection retries with backoff:
import { Pool, PoolClient } from "pg";
async function withRetry<T>(
pool: Pool,
fn: (client: PoolClient) => Promise<T>,
maxAttempts = 3
): Promise<T> {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
const client = await pool.connect();
try {
const result = await fn(client);
return result;
} catch (err) {
const isRetriable =
(err as NodeJS.ErrnoException).code === "ECONNRESET" ||
(err as { message?: string }).message?.includes("connection refused");
if (isRetriable && attempt < maxAttempts) {
await new Promise((resolve) =>
setTimeout(resolve, 100 * Math.pow(2, attempt))
);
continue;
}
throw err;
} finally {
client.release();
}
}
throw new Error("Unreachable");
}
Tradeoffs Compared to Alternatives
| Dimension | Aurora PostgreSQL | RDS PostgreSQL | Self-Managed PostgreSQL | CockroachDB | Cloud Spanner |
|---|---|---|---|---|---|
| Replication model | 6-copy quorum, shared storage | Synchronous standby (Multi-AZ) | Streaming replication (manual) | Per-range Raft groups | Per-split Paxos groups |
| Failover time | ~30s, no data loss | ~60-120s, no data loss | Minutes (manual or Patroni) | Seconds (within same region) | Seconds |
| Write scalability | Single writer, up to 128 replicas for reads | Single writer + 5 read replicas | Single writer | Distributed multi-writer | Distributed multi-writer |
| Cross-region writes | Aurora Global Database (async, ~1s RPO) | Read replica in other region (async) | Logical replication (manual) | Multi-region active-active | Synchronous multi-region |
| Consistency model | Strong (single writer) | Strong (single writer) | Strong (single writer) | Serializable | External consistency (TrueTime) |
| Storage scaling | Auto, 10GB increments, to 128TB | Up to 64TB, manual provisioning | Depends on disk | Automatic (ranges split) | Automatic (splits) |
| Operational overhead | Low (fully managed) | Low (managed, less Aurora magic) | High | Medium (managed options available) | Low (fully managed) |
| Cost at low scale | Higher than RDS | Moderate | Low (infra cost only) | Higher | Higher |
| Extension support | Limited vs. upstream PG | Full upstream PG extensions | Full | PostgreSQL wire protocol, limited extensions | No PostgreSQL extensions |
| Wire protocol compatibility | PostgreSQL or MySQL | PostgreSQL or MySQL | PostgreSQL | PostgreSQL wire protocol | Proprietary + JDBC/ODBC |
| Sweet spot | OLTP on AWS, high availability, fast failover | Standard OLTP on AWS, simpler pricing | Cost-sensitive or extension-heavy workloads | Multi-region active-active SQL | Globally distributed, strong consistency |
When to Choose Aurora
Aurora makes the most sense when you are running on AWS, need high availability with fast failover, and want to avoid the operational complexity of managing replication yourself. The 6-copy quorum provides durability and fault tolerance that exceeds what most teams achieve with self-managed PostgreSQL. The shared storage architecture makes cloning, PITR, and replica lag genuinely better than the equivalent features in RDS Multi-AZ.
The cases where Aurora is not the right choice: you need extensions that Aurora does not support, your workload demands multi-writer horizontal write scaling (Aurora has a single writer), or you are genuinely cost-constrained at small scale where Aurora’s pricing premium over RDS or a small EC2 instance is significant.
Understanding the storage architecture matters operationally. When you know that the log is the database, that storage nodes materialize pages from redo records, and that quorum acknowledgment is what commits a write, you can reason about Aurora’s behavior under failures, network partitions, and heavy write loads in a way that the “it’s just managed PostgreSQL” framing doesn’t support.
More in System Design
How CockroachDB Works Internally: Distributed SQL, Raft Consensus Per Range, and the Architecture Behind Serializable Transactions at Global Scale
A deep dive into CockroachDB's internals: the 512MB range-based data model with automatic splitting, per-range Raft replication, MVCC timestamp ordering with hybrid logical clocks, DistSQL physical planning, serializable transactions with timestamp refreshes, closed timestamps for follower reads, online schema changes using multi-version schema descriptors, and production considerations for hotspots and write amplification.
How Container Runtimes Work Internally: Namespaces, Cgroups, and the OCI Stack From Docker Run to Process Isolation
Containers are not virtual machines and they are not magic. They are a thin composition of Linux kernel primitives: namespaces, cgroups, and a layered filesystem. This article traces exactly what happens between docker run and a running process, covering the OCI spec, containerd, runc, overlay filesystems, and container networking at the kernel level.
How YugabyteDB Works Internally: DocDB Storage, Tablet Splitting, and the Dual API Architecture That Scales PostgreSQL Horizontally
A deep dive into YugabyteDB internals covering the DocDB storage engine built on RocksDB LSM trees, per-tablet Raft consensus, YSQL and YCQL dual query layers, distributed MVCC with hybrid logical clocks, automatic tablet splitting and rebalancing, xCluster multi-region replication, and production considerations for hotspots, connection pooling, and schema design.
How Cloud Spanner Works Internally: TrueTime, Paxos Replication, and the Globally-Distributed Architecture Behind Consistent Reads at Any Scale
A deep dive into Cloud Spanner's internals: TrueTime and bounded clock uncertainty, Paxos-based replication with leader leases, the read/write transaction protocol, snapshot reads without locks, interleaved table hierarchies, non-blocking schema changes, and production considerations for split management and hotspot avoidance.