Skip to main content
AWSDatabasesintermediate

AWS RDS & Aurora Deep Dive

Master AWS RDS and Aurora: engine selection, Multi-AZ, read replicas, Aurora Serverless v2, RDS Proxy, and cost optimization.

CloudToolStack Team26 min readPublished Mar 14, 2026

Prerequisites

  • Basic understanding of relational databases and SQL
  • AWS account with RDS permissions

Introduction to AWS RDS and Aurora

Amazon Relational Database Service (RDS) is a managed database service that makes it easy to set up, operate, and scale relational databases in the cloud. RDS handles time-consuming database administration tasks like hardware provisioning, database setup, patching, backups, and replication, freeing you to focus on your application. RDS supports six database engines: Amazon Aurora, PostgreSQL, MySQL, MariaDB, Oracle Database, and Microsoft SQL Server.

Amazon Aurora is AWS's cloud-native relational database engine, compatible with both MySQL and PostgreSQL. Aurora provides up to five times the throughput of standard MySQL and three times the throughput of standard PostgreSQL, along with features like automatic storage scaling up to 128 TB, up to 15 read replicas with sub-10ms replica lag, continuous backup to S3, and cross-region replication. Aurora Serverless v2 adds instant, fine-grained auto-scaling that adjusts capacity in increments as small as 0.5 Aurora Capacity Units (ACUs).

This guide covers both RDS and Aurora in depth: choosing the right engine and instance type, configuring Multi-AZ for high availability, creating and managing read replicas, implementing Aurora Serverless v2, using RDS Proxy for connection pooling, setting up automated backups and point-in-time recovery, monitoring performance with Performance Insights, and optimizing costs.

RDS Free Tier

The AWS Free Tier includes 750 hours per month of RDS Single-AZ db.t2.micro (or db.t3.micro/db.t4g.micro for some engines) instances for 12 months. This includes 20 GB of General Purpose SSD storage, 20 GB of backup storage, and supports MySQL, PostgreSQL, MariaDB, Oracle BYOL, and SQL Server Express editions. Aurora is not included in the Free Tier but offers a free trial for new Aurora customers.

Choosing the Right Database Engine

Selecting a database engine is one of the most important architectural decisions. Each engine has strengths for different workload types, licensing models, and ecosystem compatibility.

EngineBest ForKey AdvantageAurora Compatible
Aurora MySQLMySQL workloads needing performance5x MySQL throughput, auto-scaling storageYes
Aurora PostgreSQLPostgreSQL workloads needing performance3x PostgreSQL throughput, Babelfish for SQL Server migrationYes
RDS MySQLStandard MySQL workloadsFull MySQL compatibility, lower cost for small DBsNo
RDS PostgreSQLStandard PostgreSQL workloadsFull PostgreSQL extension supportNo
RDS SQL Server.NET/Windows applicationsNative SQL Server features, SSRS/SSISNo
RDS OracleOracle Database workloadsLicense included or BYOL optionsNo

Creating an RDS Instance

Creating an RDS instance involves choosing the engine, instance class, storage type, and networking configuration. For production workloads, always use Multi-AZ deployment, encryption at rest, and place the instance in a private subnet.

bash
# Create a DB subnet group (spans multiple AZs)
aws rds create-db-subnet-group \
  --db-subnet-group-name production-db-subnets \
  --db-subnet-group-description "Private subnets for RDS" \
  --subnet-ids '["subnet-abc123", "subnet-def456", "subnet-ghi789"]'

# Create a security group for the database
SG_ID=$(aws ec2 create-security-group \
  --group-name rds-production \
  --description "Security group for production RDS" \
  --vpc-id vpc-xxx \
  --query 'GroupId' \
  --output text)

aws ec2 authorize-security-group-ingress \
  --group-id $SG_ID \
  --protocol tcp \
  --port 5432 \
  --source-group sg-app-servers

# Create a Multi-AZ PostgreSQL instance
aws rds create-db-instance \
  --db-instance-identifier production-api-db \
  --db-instance-class db.r6g.xlarge \
  --engine postgres \
  --engine-version "16.2" \
  --master-username dbadmin \
  --manage-master-user-password \
  --allocated-storage 100 \
  --storage-type gp3 \
  --storage-throughput 500 \
  --iops 3000 \
  --multi-az \
  --db-subnet-group-name production-db-subnets \
  --vpc-security-group-ids $SG_ID \
  --db-name apidb \
  --backup-retention-period 14 \
  --preferred-backup-window "03:00-04:00" \
  --preferred-maintenance-window "sun:04:00-sun:05:00" \
  --storage-encrypted \
  --kms-key-id alias/rds-encryption \
  --enable-performance-insights \
  --performance-insights-retention-period 731 \
  --monitoring-interval 60 \
  --monitoring-role-arn arn:aws:iam::123456789:role/rds-monitoring \
  --auto-minor-version-upgrade \
  --copy-tags-to-snapshot \
  --deletion-protection \
  --tags Key=Environment,Value=production Key=Team,Value=platform

# Wait for the instance to be available
aws rds wait db-instance-available \
  --db-instance-identifier production-api-db

# Get the endpoint
aws rds describe-db-instances \
  --db-instance-identifier production-api-db \
  --query 'DBInstances[0].{Endpoint: Endpoint.Address, Port: Endpoint.Port, Status: DBInstanceStatus, MultiAZ: MultiAZ, Storage: AllocatedStorage}' \
  --output table

Managed Master Passwords

Use --manage-master-user-password instead of specifying a password directly. AWS automatically generates and stores the password in Secrets Manager, rotates it periodically, and provides the secret ARN. This eliminates hardcoded passwords in scripts and infrastructure code. Your application retrieves the password from Secrets Manager at runtime.

Amazon Aurora Deep Dive

Aurora's architecture fundamentally differs from standard RDS. Instead of using traditional block storage, Aurora uses a distributed, fault-tolerant, self-healing storage system that automatically replicates data six ways across three Availability Zones. The storage layer is decoupled from the compute layer, allowing each to scale independently. Storage automatically grows from 10 GB to 128 TB without downtime, and you only pay for storage actually used.

bash
# Create an Aurora PostgreSQL cluster
aws rds create-db-cluster \
  --db-cluster-identifier production-aurora \
  --engine aurora-postgresql \
  --engine-version "16.1" \
  --master-username dbadmin \
  --manage-master-user-password \
  --db-subnet-group-name production-db-subnets \
  --vpc-security-group-ids sg-xxx \
  --database-name appdb \
  --backup-retention-period 14 \
  --preferred-backup-window "03:00-04:00" \
  --storage-encrypted \
  --kms-key-id alias/aurora-encryption \
  --enable-cloudwatch-logs-exports '["postgresql"]' \
  --deletion-protection \
  --tags Key=Environment,Value=production

# Create the writer instance
aws rds create-db-instance \
  --db-instance-identifier production-aurora-writer \
  --db-cluster-identifier production-aurora \
  --db-instance-class db.r6g.2xlarge \
  --engine aurora-postgresql \
  --availability-zone us-east-1a

# Create reader instances for read scaling
for i in 1 2; do
  aws rds create-db-instance \
    --db-instance-identifier "production-aurora-reader-$i" \
    --db-cluster-identifier production-aurora \
    --db-instance-class db.r6g.xlarge \
    --engine aurora-postgresql \
    --availability-zone "us-east-1$(echo a b c | cut -d' ' -f$((i)))"
done

# Get cluster endpoints
aws rds describe-db-clusters \
  --db-cluster-identifier production-aurora \
  --query 'DBClusters[0].{Writer: Endpoint, Reader: ReaderEndpoint, Status: Status, Members: DBClusterMembers[].DBInstanceIdentifier}' \
  --output json

Aurora Serverless v2

Aurora Serverless v2 automatically scales compute capacity based on your application's workload. Unlike provisioned instances where you choose a fixed instance size, Serverless v2 scales in fine-grained increments of 0.5 ACUs (Aurora Capacity Units) between a minimum and maximum you define. Scaling happens in-place within seconds, without any connection disruption. This makes it ideal for variable workloads, development environments, and applications with unpredictable traffic patterns.

bash
# Create an Aurora cluster with Serverless v2 configuration
aws rds create-db-cluster \
  --db-cluster-identifier app-serverless \
  --engine aurora-postgresql \
  --engine-version "16.1" \
  --master-username dbadmin \
  --manage-master-user-password \
  --db-subnet-group-name production-db-subnets \
  --vpc-security-group-ids sg-xxx \
  --database-name appdb \
  --serverless-v2-scaling-configuration MinCapacity=0.5,MaxCapacity=32 \
  --storage-encrypted

# Create a Serverless v2 writer instance
aws rds create-db-instance \
  --db-instance-identifier app-serverless-writer \
  --db-cluster-identifier app-serverless \
  --db-instance-class db.serverless \
  --engine aurora-postgresql

# Create a Serverless v2 reader
aws rds create-db-instance \
  --db-instance-identifier app-serverless-reader \
  --db-cluster-identifier app-serverless \
  --db-instance-class db.serverless \
  --engine aurora-postgresql

# Monitor current ACU utilization
aws cloudwatch get-metric-statistics \
  --namespace AWS/RDS \
  --metric-name ACUUtilization \
  --dimensions Name=DBClusterIdentifier,Value=app-serverless \
  --start-time $(date -u -v-1H '+%Y-%m-%dT%H:%M:%S') \
  --end-time $(date -u '+%Y-%m-%dT%H:%M:%S') \
  --period 300 \
  --statistics Average Maximum \
  --output table

# Monitor ServerlessDatabaseCapacity (current ACUs)
aws cloudwatch get-metric-statistics \
  --namespace AWS/RDS \
  --metric-name ServerlessDatabaseCapacity \
  --dimensions Name=DBClusterIdentifier,Value=app-serverless \
  --start-time $(date -u -v-1H '+%Y-%m-%dT%H:%M:%S') \
  --end-time $(date -u '+%Y-%m-%dT%H:%M:%S') \
  --period 60 \
  --statistics Average \
  --output table

Serverless v2 Cost Considerations

Serverless v2 charges per ACU-hour, which is approximately 15-20% more expensive per ACU than the equivalent provisioned instance capacity. The cost advantage comes from scaling down during low-traffic periods. If your workload runs at high capacity 24/7, provisioned instances are cheaper. For workloads with significant variation (e.g., heavy during business hours, light overnight), Serverless v2 can save 30-60% compared to provisioning for peak capacity. Set the minimum ACU to 0.5 for development and to a higher value for production to avoid cold start latency.

RDS Proxy

RDS Proxy is a fully managed database proxy that sits between your application and the RDS database. It pools and shares database connections, reducing the stress on the database from many short-lived connections (common in serverless and microservice architectures). RDS Proxy also improves application availability by automatically connecting to a standby instance during Multi-AZ failover, often reducing failover time from 30+ seconds to under 1 second.

bash
# Create an RDS Proxy
aws rds create-db-proxy \
  --db-proxy-name production-api-proxy \
  --engine-family POSTGRESQL \
  --auth '[{
    "AuthScheme": "SECRETS",
    "SecretArn": "arn:aws:secretsmanager:us-east-1:123456789:secret:rds-master-password",
    "IAMAuth": "REQUIRED"
  }]' \
  --role-arn arn:aws:iam::123456789:role/rds-proxy-role \
  --vpc-subnet-ids subnet-abc123 subnet-def456 \
  --vpc-security-group-ids sg-proxy \
  --require-tls

# Register the target (database instance)
aws rds register-db-proxy-targets \
  --db-proxy-name production-api-proxy \
  --db-cluster-identifiers production-aurora

# Get the proxy endpoint
aws rds describe-db-proxies \
  --db-proxy-name production-api-proxy \
  --query 'DBProxies[0].{Endpoint: Endpoint, Status: Status, EngineFamily: EngineFamily}' \
  --output table

# Your application connects to the proxy endpoint instead of the database endpoint
# Connection string: postgresql://dbadmin@production-api-proxy.proxy-xxx.us-east-1.rds.amazonaws.com:5432/appdb

# Monitor proxy connection metrics
aws cloudwatch get-metric-statistics \
  --namespace AWS/RDS \
  --metric-name DatabaseConnections \
  --dimensions Name=ProxyName,Value=production-api-proxy \
  --start-time $(date -u -v-1H '+%Y-%m-%dT%H:%M:%S') \
  --end-time $(date -u '+%Y-%m-%dT%H:%M:%S') \
  --period 60 \
  --statistics Average Maximum \
  --output table

Backup, Recovery, and Replication

RDS provides two types of backups: automated backups (continuous, point-in-time recovery up to your retention period) and manual snapshots (persisted until you explicitly delete them). Aurora additionally supports backtracking, which lets you rewind the database to a specific point in time without restoring from a backup.

bash
# Create a manual snapshot
aws rds create-db-snapshot \
  --db-instance-identifier production-api-db \
  --db-snapshot-identifier pre-migration-snapshot-2026-03-14

# Create a cluster snapshot (Aurora)
aws rds create-db-cluster-snapshot \
  --db-cluster-identifier production-aurora \
  --db-cluster-snapshot-identifier pre-migration-2026-03-14

# Restore to a point in time (creates a new instance)
aws rds restore-db-instance-to-point-in-time \
  --source-db-instance-identifier production-api-db \
  --target-db-instance-identifier production-api-db-restored \
  --restore-time "2026-03-14T09:30:00Z" \
  --db-instance-class db.r6g.xlarge \
  --db-subnet-group-name production-db-subnets \
  --vpc-security-group-ids sg-xxx

# Cross-region read replica for DR
aws rds create-db-instance-read-replica \
  --db-instance-identifier production-api-db-us-west-2 \
  --source-db-instance-identifier arn:aws:rds:us-east-1:123456789:db:production-api-db \
  --db-instance-class db.r6g.xlarge \
  --region us-west-2

# Aurora Global Database (cross-region replication, <1s replication lag)
aws rds create-global-cluster \
  --global-cluster-identifier production-global \
  --source-db-cluster-identifier arn:aws:rds:us-east-1:123456789:cluster:production-aurora \
  --engine aurora-postgresql

# Add a secondary region to the global cluster
aws rds create-db-cluster \
  --db-cluster-identifier production-aurora-us-west-2 \
  --engine aurora-postgresql \
  --global-cluster-identifier production-global \
  --db-subnet-group-name dr-subnets \
  --region us-west-2

Performance Insights and Monitoring

Performance Insights is a database performance tuning and monitoring feature that helps you quickly assess the load on your database and determine when and where to take action. It visualizes database load by wait types, SQL statements, hosts, and users, making it easy to identify the queries causing performance bottlenecks.

bash
# Enable Performance Insights on an existing instance
aws rds modify-db-instance \
  --db-instance-identifier production-api-db \
  --enable-performance-insights \
  --performance-insights-retention-period 731 \
  --performance-insights-kms-key-id alias/pi-encryption \
  --apply-immediately

# Get Performance Insights data for top SQL statements
aws pi get-resource-metrics \
  --service-type RDS \
  --identifier db-XXXXXXXXXXXX \
  --metric-queries '[
    {"Metric": "db.load.avg", "GroupBy": {"Group": "db.sql", "Limit": 10}}
  ]' \
  --start-time $(date -u -v-1H '+%Y-%m-%dT%H:%M:%S') \
  --end-time $(date -u '+%Y-%m-%dT%H:%M:%S') \
  --period-in-seconds 60

# Key CloudWatch metrics to monitor
# CPUUtilization - Should stay below 80%
# FreeableMemory - Monitor for memory pressure
# ReadIOPS / WriteIOPS - Storage I/O activity
# DatabaseConnections - Active connection count
# FreeStorageSpace - Available storage
# ReplicaLag - Read replica delay (seconds)
# DiskQueueDepth - Pending I/O requests

# Create alarms for critical metrics
aws cloudwatch put-metric-alarm \
  --alarm-name rds-high-cpu \
  --alarm-description "RDS CPU utilization above 80%" \
  --namespace AWS/RDS \
  --metric-name CPUUtilization \
  --dimensions Name=DBInstanceIdentifier,Value=production-api-db \
  --statistic Average \
  --period 300 \
  --evaluation-periods 3 \
  --threshold 80 \
  --comparison-operator GreaterThanThreshold \
  --alarm-actions arn:aws:sns:us-east-1:123456789:database-alerts

aws cloudwatch put-metric-alarm \
  --alarm-name rds-low-storage \
  --alarm-description "RDS storage space below 10 GB" \
  --namespace AWS/RDS \
  --metric-name FreeStorageSpace \
  --dimensions Name=DBInstanceIdentifier,Value=production-api-db \
  --statistic Average \
  --period 300 \
  --evaluation-periods 1 \
  --threshold 10737418240 \
  --comparison-operator LessThanThreshold \
  --alarm-actions arn:aws:sns:us-east-1:123456789:database-alerts

Cost Optimization Strategies

RDS and Aurora can be significant cost centers. Here are proven strategies to reduce database spending without sacrificing performance or availability.

Cost Optimization Approaches

StrategySavingsTrade-off
Reserved Instances (1yr)30-40%Commit to specific instance type for 1 year
Reserved Instances (3yr)50-60%Longer commitment, less flexibility
Aurora Serverless v230-60% for variable workloadsSlightly higher per-ACU cost
Right-sizing instances20-50%Requires performance testing
Graviton instances (db.r7g)10-20%ARM architecture, verify compatibility
gp3 storage (vs gp2)20%No trade-off, strictly better

Graviton-Based Instances

AWS Graviton processors (db.r7g, db.r6g, db.m7g families) provide up to 20% better price-performance than equivalent x86 instances. Aurora, PostgreSQL, MySQL, and MariaDB on RDS all support Graviton instances. For most applications, switching from db.r6i to db.r7g is transparent because the database engine handles the architecture difference. Always test your specific workload before migrating production.

RDS and Aurora are the backbone of most AWS application architectures. Choose standard RDS for straightforward relational database needs with full engine compatibility. Choose Aurora when you need higher performance, automatic storage scaling, fast read replicas, or global distribution. Use Serverless v2 for variable workloads, and RDS Proxy for serverless and microservice architectures with many short-lived connections. Always implement Multi-AZ for production, configure automated backups, and monitor with Performance Insights.

Getting Started with AWSAmazon OpenSearch Service GuideAmazon Cognito Guide

Key Takeaways

  1. 1Aurora provides up to 5x MySQL and 3x PostgreSQL throughput with auto-scaling storage up to 128 TB.
  2. 2Aurora Serverless v2 scales in 0.5 ACU increments for variable workloads without connection disruption.
  3. 3RDS Proxy pools connections and reduces Multi-AZ failover time from 30+ seconds to under 1 second.
  4. 4Graviton-based instances (db.r7g) provide 10-20% better price-performance than x86 equivalents.

Frequently Asked Questions

When should I choose Aurora over standard RDS?
Choose Aurora when you need higher throughput, automatic storage scaling (up to 128 TB), fast read replicas (up to 15 with sub-10ms lag), or global distribution. Choose standard RDS for full engine compatibility, lower cost for small databases, or when you need Oracle or SQL Server engines. Aurora costs approximately 20% more than equivalent RDS instances but delivers significantly better performance.
How does Aurora Serverless v2 pricing compare to provisioned?
Serverless v2 charges per ACU-hour, approximately 15-20% more expensive per ACU than equivalent provisioned capacity. The cost advantage comes from scaling down during low-traffic periods. If your workload runs at high capacity 24/7, provisioned is cheaper. For variable workloads, Serverless v2 can save 30-60%.

Written by CloudToolStack Team

Cloud engineers and architects with hands-on experience across AWS, Azure, and GCP. We write guides based on real-world production patterns, not just documentation rewrites.

Disclaimer: This guide is for educational purposes. Cloud services change frequently; always refer to official documentation for the latest information. AWS, Azure, and GCP are trademarks of their respective owners.