What it actually is
Aurora is a MySQL-compatible and PostgreSQL-compatible database where the engine and the storage are separate services. Your instances run a modified engine that does the SQL work. Underneath them sits one distributed cluster volume that belongs to the cluster, not to any instance. Instances are stateless compute attached to that volume.
That is the whole idea, and it explains almost everything else. A replica in plain RDS has its own copy of the data and applies the primary's changes. An Aurora replica has no copy at all: it reads the same volume as the writer. Adding a reader adds compute, not another set of disks. Promoting a reader to writer does not move data. Restoring from backup, cloning, and growing storage are operations on the volume, not on a host.
What AWS operates: the host, the engine patches you schedule, the replicated storage, the continuous backup, and the failover machinery. What you operate: the schema, the queries, how many instances you attach and how big they are, the I/O billing mode, network access, and how your client behaves when the writer changes. That last item is the one the lab below makes visible.
The model
The primitives are the DB cluster, the cluster volume, and the DB instances. The cluster is the unit you create. It has one writer instance and up to 15 read-only Aurora Replicas. Per the docs, the cluster volume is a single virtual volume on SSDs that holds copies of the data across three Availability Zones in one Region. The FAQ gives the detail: the volume is divided into 10 GiB segments, and each segment is replicated six ways across the three AZs. The docs describe the tolerance as losing up to two copies without losing write availability and up to three copies without losing read availability. The storage is also described as self-healing: blocks and disks are continuously scanned and repaired.
Two consequences matter in practice. First, replication is independent of how many instances you run. A one-instance cluster still has six copies of its data. Second, the volume grows on its own and you pay only for the space you use. A cluster volume can reach 256 TiB on newer engine versions (Aurora PostgreSQL 17.5+, 16.9+ and 15.13+, Aurora MySQL 3.10+) and 128 TiB on earlier ones. You do not provision storage or IOPS.
Replica lag follows from the shared volume. The FAQ says replicas in the same Region have virtually no replication lag, typically tens of milliseconds, because they read the same volume as the primary. Cross-Region replicas in an Aurora Global Database use physical replication with typical lag under a second.
You reach the cluster through endpoints. The cluster endpoint always points at the current writer and moves on failover. The reader endpoint balances connections among the Aurora Replicas. Custom endpoints target a subset of instances, useful when you have a larger replica for analytics. Instance endpoints point at one specific instance and do not follow failovers, so applications should not use them for normal traffic.
Compute comes in two shapes that you can mix in one cluster. Provisioned instances use a class such as db.r6g.large. Serverless v2 instances use the class db.serverless and scale between a minimum and maximum capacity in Aurora Capacity Units. One ACU is roughly 2 GiB of memory with matching CPU and networking. Capacity moves in steps as small as 0.5 ACU, is measured every second, and the docs say scaling does not disrupt open connections or in-flight transactions. The supported range is 0 to 256 ACUs on recent engine versions.
When to use it, when not to
Aurora | RDS (Multi-AZ instance) | DynamoDB | |
|---|---|---|---|
Data model | Relational, MySQL or PostgreSQL | Relational, six engines | Key-value and document |
Storage | Shared cluster volume, six copies, 3 AZs | EBS per instance plus a standby copy | Managed, partitioned |
Read scaling | Up to 15 replicas on the same volume | Separate read replicas with their own storage | Add read capacity or DAX |
Failover | Promote a replica, typically under 30 seconds | Standby promotion | No failover concept |
You size | Instances (or ACU range) | Instance and storage | Capacity mode and keys |
Pick it when | Relational at scale, many readers, variable load | Engines Aurora lacks, smaller predictable load, lower cost | Known access patterns, huge scale |
Pick Aurora when you want relational semantics and your read traffic or your recovery requirements justify the architecture: several readers, fast failover, large or fast-growing data, or a spiky load that suits Serverless v2. Pick plain RDS when you need Oracle, SQL Server or Db2, or when a single modest instance is genuinely enough and the lower baseline cost matters. Pick DynamoDB when your access patterns are known and you want to stop thinking about instances altogether. There is an earlier deep dive on the RDS side in this series: SL#129 on RDS covers what Multi-AZ managed means and measures a failover the same way we do here.
What it costs
Aurora bills several things independently. Compute is billed per instance-hour for provisioned instances or per ACU-hour (metered per second) for Serverless v2. Storage is billed per GB-month for what you use. I/O is billed per million requests in the Standard configuration and is free in I/O-Optimized. Backup storage beyond 100 percent of your cluster size is billed, and data transfer applies as usual.
The pricing page's worked examples for US East (N. Virginia) show the shape. In Aurora Standard: a db.r6i.large On-Demand instance at $0.29 per hour, Serverless at $0.12 per ACU-hour, storage at $0.10 per GB-month, and I/O at $0.20 per million requests. In I/O-Optimized: $0.377 per hour for the same instance, $0.156 per ACU-hour, storage at $0.225 per GB-month, and no read or write I/O charge. Check the live page for your Region before you budget, because these are examples from one page on one day.
The line that surprises people is I/O. In Standard it is a metered cost that tracks how chatty your workload is, and a busy database can generate an I/O bill that rivals the compute bill. The docs give a rule of thumb: consider I/O-Optimized when I/O spend is 25 percent or more of your total Aurora spend, and the pricing page says savings can reach 40 percent in that case. You can switch between the two modes once every 30 days, and on non-NVMe-based instances the switch has no downtime. The second surprise is that Serverless v2 is not automatically cheaper. A db.r6i.large has 16 GiB of memory, which is about 8 ACUs; at the example rates, 8 ACUs held steady for an hour is $0.96 against $0.29 for the provisioned instance. Serverless v2 pays off when load is spiky or idle most of the day, not when it is flat, and every extra reader multiplies your idle floor.
Free tier reality: the pricing page lists Aurora PostgreSQL Serverless on its Free plan with up to 4 ACUs and 1 GiB of storage per cluster, plus sign-up credits. Treat the lab below as a paid exercise and verify what your account actually gets.
Estimated cost to follow along: two Serverless v2 instances at a 0.5 ACU floor is 1 ACU-hour per hour, about $0.12 per hour at the example Standard rate, plus a few cents of storage and I/O. Two hours of work is well under a dollar if you clean up at the end. If the cluster scales above the floor during the prober, it is still cents.
The limits that bite
Per account per Region the defaults are 40 DB clusters and 40 DB instances, both adjustable. The instance quota is shared with RDS, Neptune and DocumentDB instances, so an account full of RDS databases has less room than it looks. A cluster has up to 15 Aurora Replicas and that cannot be adjusted for Aurora. There are 50 DB cluster parameter groups and 50 DB subnet groups by default, and a subnet group holds at most 20 subnets.
Storage has ceilings too: 256 TiB or 128 TiB for the cluster volume depending on engine version, 64 TiB maximum table size on Aurora MySQL and 32 TiB on Aurora PostgreSQL. Partition large tables long before you meet those numbers.
Failover has its own limits. With replicas, the docs say service is typically restored in under 60 seconds and often under 30. With no replica, Aurora recreates the primary in the same AZ, which typically takes under 10 minutes. A single-instance cluster is therefore not a high-availability design, whatever the storage does. AWS recommends at least one reader in a second AZ.
Replicas have promotion priorities from 0 (highest) to 15. Within a tier Aurora promotes the largest replica first. If your only reader is smaller than the writer, a failover can leave you running on less compute than you had.
Serverless v2 adds its own traps. The ACU range you can actually use is limited by the lower of your engine version and platform version, with older versions topping out at 128 ACUs or lacking the 0 ACU minimum. Scaling to 0 needs recent engine versions (Aurora PostgreSQL 16.3, 15.7, 14.12 or 13.15 and later, Aurora MySQL 3.08.0 and later). Auto-pause will not engage with any open connection, an attached RDS Proxy, logical replication, or in a Global Database primary, and resume takes about 15 seconds, longer after a day idle. Readers in promotion tiers 0 and 1 track the writer's capacity so they are ready to take over, which means they also cost what the writer costs. Readers in tiers 2 to 15 scale on their own and may need to scale up after a promotion.
Build it
The lab: a Serverless v2 Aurora PostgreSQL cluster with a writer and a reader in different AZs, a prober that connects to the cluster endpoint every 250 milliseconds, a manual failover, and a measured outage. The commands follow the Aurora User Guide pages cited below; they were written from the documentation and not executed against a live account for this article, so run the first commands in a scratch account and read the output before trusting anything.
Prerequisites
You need the AWS CLI v2 configured with a Region and credentials, Python 3.10 or later with the psycopg driver, and a default VPC with subnets in at least two AZs. The lab makes the instances publicly accessible and locks the security group to your own IP so you can run the prober from your laptop. In a real system you would run the client inside the VPC, and you should do that for anything beyond this exercise.
The IAM permissions for the identity you use, in a scratch account: rds:CreateDBCluster, rds:CreateDBInstance, rds:DescribeDBClusters, rds:DescribeDBInstances, rds:DescribeDBEngineVersions, rds:FailoverDBCluster, rds:ModifyDBCluster, rds:DeleteDBInstance, rds:DeleteDBCluster, ec2:DescribeVpcs, ec2:DescribeSubnets, ec2:CreateSecurityGroup, ec2:AuthorizeSecurityGroupIngress, ec2:DeleteSecurityGroup, plus secretsmanager:GetSecretValue for the managed master secret. A broad scratch-account role is fine for the lab; scope it down in production.
Step 1: network and engine version
export AWS_REGION=eu-west-1
export CLUSTER=sl130-aurora
VPC=$(aws ec2 describe-vpcs --filters Name=isDefault,Values=true --query 'Vpcs[0].VpcId' --output text)
SG=$(aws ec2 create-security-group --group-name sl130-aurora-sg --description "sl130 lab" --vpc-id $VPC --query GroupId --output text)
MYIP=$(curl -s https://checkip.amazonaws.com)
aws ec2 authorize-security-group-ingress --group-id $SG --protocol tcp --port 5432 --cidr ${MYIP}/32
VER=$(aws rds describe-db-engine-versions --engine aurora-postgresql --default-only --query 'DBEngineVersions[0].EngineVersion' --output text)
echo $VERAsk the API for the default version rather than hardcoding one, because versions turn over constantly. For scale to zero you would need 16.3 or later in the 16 line, or an equivalent minor in another line; this lab uses a 0.5 ACU floor so it works on any Serverless v2 compatible version.
Step 2: create the cluster and two instances
aws rds create-db-cluster \
--db-cluster-identifier $CLUSTER \
--engine aurora-postgresql --engine-version $VER \
--serverless-v2-scaling-configuration MinCapacity=0.5,MaxCapacity=4 \
--master-username labadmin --manage-master-user-password \
--vpc-security-group-ids $SG \
--no-deletion-protection
aws rds create-db-instance --db-cluster-identifier $CLUSTER \
--db-instance-identifier ${CLUSTER}-writer \
--db-instance-class db.serverless --engine aurora-postgresql \
--publicly-accessible --availability-zone ${AWS_REGION}a
aws rds create-db-instance --db-cluster-identifier $CLUSTER \
--db-instance-identifier ${CLUSTER}-reader \
--db-instance-class db.serverless --engine aurora-postgresql \
--publicly-accessible --availability-zone ${AWS_REGION}b \
--promotion-tier 1
aws rds wait db-instance-available --db-instance-identifier ${CLUSTER}-writer
aws rds wait db-instance-available --db-instance-identifier ${CLUSTER}-readerThree details worth noticing. The CLI shows the engine mode as provisioned even for Serverless v2, which is expected per the docs. The instance class db.serverless is what makes an instance Serverless v2. And the reader is in promotion tier 1, so it tracks the writer's capacity and is a ready failover target. The AZ letters are examples for eu-west-1; if your Region lacks them, list the AZs in your default subnets and adjust.
Step 3: read the endpoints and the managed secret
aws rds describe-db-clusters --db-cluster-identifier $CLUSTER \
--query 'DBClusters[0].{Writer:Endpoint,Reader:ReaderEndpoint,Secret:MasterUserSecret.SecretArn,Members:DBClusterMembers[].[DBInstanceIdentifier,IsClusterWriter]}'Save the writer endpoint and the secret ARN. Fetch the password with aws secretsmanager get-secret-value --secret-id <arn> --query SecretString --output text; it is a JSON document with the username and password.
Step 4: the prober
import os, time, json, psycopg
HOST = os.environ["WRITER_ENDPOINT"]
PW = os.environ["PGPASSWORD"]
last_ok = None
down_since = None
events = []
while True:
t = time.time()
try:
with psycopg.connect(host=HOST, user="labadmin", password=PW,
dbname="postgres", connect_timeout=2,
autocommit=True, sslmode="require") as c:
row = c.execute("select pg_is_in_recovery(), inet_server_addr()").fetchone()
if row[0]:
raise RuntimeError("connected to a reader")
if down_since:
events.append({"down_s": round(t - down_since, 2), "server": str(row[1])})
print(json.dumps(events[-1]), flush=True)
down_since = None
last_ok = t
except Exception as e:
if down_since is None:
down_since = last_ok or t
print("DOWN:", type(e).__name__, flush=True)
time.sleep(0.25)Run it with WRITER_ENDPOINT set to the cluster endpoint and PGPASSWORD set from the secret. A healthy run prints nothing. The check on pg_is_in_recovery() matters: a connection that lands on a reader counts as down for a write client, which is how the docs say the reader endpoint may behave briefly after a promotion if you use it by mistake.
Step 5: force a failover and read the clock
In another terminal:
aws rds failover-db-cluster --db-cluster-identifier $CLUSTER \
--target-db-instance-identifier ${CLUSTER}-readerThe cluster status shows Failing-over and returns to Available. The prober prints DOWN and then a line with the measured outage and the new server address. The docs say to expect under 60 seconds and often under 30. Your number depends on your client: a prober that opens a fresh connection each time will see the DNS change as soon as the record flips, while an application with a long DNS cache or a pool of dead connections will see a longer outage that has nothing to do with Aurora. Run it three times, swapping the target each time, and compare.
Verification
Confirm the roles swapped with the same describe-db-clusters query: IsClusterWriter should now be true for the former reader. Then confirm the shared volume story directly. Write a row through the cluster endpoint, then read it through the reader endpoint:
psql "host=$WRITER_ENDPOINT user=labadmin dbname=postgres sslmode=require" \
-c "create table if not exists t(i int); insert into t values (1);"
psql "host=$READER_ENDPOINT user=labadmin dbname=postgres sslmode=require" \
-c "select * from t;"The row is visible on the replica within tens of milliseconds, with no replication setup, because both instances read one volume. Finally, open the CloudWatch metric ServerlessDatabaseCapacity for the cluster and watch the ACU value sit near the floor while idle.
When it breaks
Connection timeouts right after creation usually mean the security group, the public accessibility flag, or a wrong endpoint, not Aurora. Authentication failures with the managed secret usually mean you used the secret ARN as the password; the password is inside the JSON. An error on create-db-instance about the instance class or the missing scaling configuration means the cluster has no Serverless v2 capacity range yet; the docs say you cannot add db.serverless instances until you set one with modify-db-cluster.
If your outage during failover is far longer than a minute, check three things in order: whether the client cached the old DNS answer, whether a connection pool kept handing out dead sockets, and whether the cluster had a replica at all. With no replica Aurora recreates the writer, which can take minutes. If the application sees read-only errors after a failover, it connected to the former writer that became a reader, or to an instance endpoint. Use the cluster endpoint for writes.
If scale to zero never pauses, it is almost always an open idle connection, an RDS Proxy, or logical replication. If a resumed database feels slow, remember the docs say it comes back at a small capacity and scales up from there, and set client timeouts above 15 seconds.
Cleanup
aws rds delete-db-instance --db-instance-identifier ${CLUSTER}-reader --skip-final-snapshot
aws rds delete-db-instance --db-instance-identifier ${CLUSTER}-writer --skip-final-snapshot
aws rds wait db-instance-deleted --db-instance-identifier ${CLUSTER}-reader
aws rds wait db-instance-deleted --db-instance-identifier ${CLUSTER}-writer
aws rds delete-db-cluster --db-cluster-identifier $CLUSTER --skip-final-snapshot
aws ec2 delete-security-group --group-id $SGThen confirm with aws rds describe-db-clusters --query 'DBClusters[].DBClusterIdentifier' that the list no longer includes the lab cluster, and check Cost Explorer the next day. Deleting a cluster stops compute and storage charges for it; the managed master secret is removed with the cluster, but check Secrets Manager for any leftover. Skipping the final snapshot is right for a lab and wrong for anything with data you care about.

