AWS Aurora Serverless: Auto-Scaling Relational Database

Aurora Serverless v2 is Amazon's answer to a long-standing challenge in cloud databases: how do you run a fully relational, ACID-compliant database that scales instantly with your traffic, charges you only for what you use, and never leaves you managing cluster capacity? This guide covers everything you need to know about Aurora Serverless v2 — from its ACU scaling model and zero cold-start architecture to the Data API, RDS Proxy integration, cost analysis, and step-by-step migration from RDS MySQL or PostgreSQL.

Aurora Serverless v2 Overview

Amazon Aurora Serverless v2, generally available since April 2022, is a serverless configuration of Amazon Aurora that automatically adjusts database capacity based on application demand. Unlike traditional Aurora provisioned clusters where you select a fixed instance class (e.g., db.r6g.xlarge), Aurora Serverless v2 measures capacity in Aurora Capacity Units (ACUs) — fractional compute-and-memory slices that can scale in 0.5 ACU increments.

Each ACU provides approximately 2 GB of memory along with corresponding CPU and networking resources. You set a minimum and maximum ACU range per cluster — for example, 0.5 ACU minimum to 128 ACU maximum. Aurora Serverless v2 continuously monitors the database workload and scales within that range in under a second, far faster than the cold-start pause that plagued Aurora Serverless v1.

Key Capabilities

  • Instant scale-up: Aurora Serverless v2 scales in fine-grained increments (0.5 ACU) in less than one second. There is no brief capacity-change pause visible to clients.
  • Scale-to-low (not scale-to-zero): At minimum ACU of 0.5, the writer instance stays live at all times. This eliminates cold starts entirely — the trade-off versus v1 is a small baseline cost even at idle.
  • Reader auto-scaling: You can add Aurora Serverless v2 reader instances. AWS can automatically add and remove readers when you enable Auto Scaling policies on the cluster.
  • Compatible with Aurora features: Unlike v1, v2 supports Multi-AZ, Aurora Global Database, RDS Proxy, Performance Insights, Enhanced Monitoring, and the Aurora Data API — all at the same time.
  • Engine compatibility: Aurora Serverless v2 supports Aurora MySQL 8.0 (compatible with MySQL 8.0) and Aurora PostgreSQL 13, 14, 15, and 16.
ACU Sizing Rule of Thumb: 1 ACU ≈ 2 GB RAM + proportional vCPU. A typical web app database with moderate traffic fits well between 2–16 ACU. OLAP or large analytical queries may need 32–128 ACU bursts.

Aurora Serverless v1 vs v2 Comparison

Aurora Serverless v1 was introduced in 2018 and offered scale-to-zero capability for intermittent workloads. However, its slow scaling (minutes) and limited feature support made it impractical for production applications. Aurora Serverless v2 was re-architected from the ground up.

Feature Aurora Serverless v1 Aurora Serverless v2
Scale-to-zero Yes (pauses after inactivity) No (minimum 0.5 ACU always running)
Scale-up speed Minutes (causes brief connection drop) Sub-second, no client interruption
Scaling granularity Fixed doubling steps (1, 2, 4, 8 ACU) 0.5 ACU increments (continuous)
Multi-AZ reader support No Yes (multiple reader instances)
Aurora Global Database No Yes
RDS Proxy No Yes
Data API Yes Yes
Performance Insights No Yes
Supported engines MySQL 5.6/5.7, PostgreSQL 10 MySQL 8.0, PostgreSQL 13–16
Max ACU 256 (MySQL), 384 (PostgreSQL) 128 (both, per instance; cluster scales via readers)
Ideal workload Dev/test, truly intermittent jobs Variable production workloads

The recommendation from AWS and most practitioners: use Aurora Serverless v2 for all new workloads. v1 only makes economic sense if your database is genuinely idle for hours at a time (e.g., a nightly batch job) and the cold-start penalty is acceptable. AWS announced end-of-life for v1 — new clusters should use v2.

Aurora vs RDS vs DynamoDB: When to Use Each

Three AWS database services compete in the relational/NoSQL space. The right choice depends on your data model, consistency requirements, and operational posture.

Criterion Aurora Serverless v2 RDS (Provisioned) DynamoDB
Data model Relational (SQL) Relational (SQL) Key-value / document (NoSQL)
Schema Fixed schema, migrations required Fixed schema Schema-less (flexible)
Capacity management Auto (ACU range) Manual (instance class) Auto (on-demand or provisioned WCU/RCU)
Scaling speed Sub-second Minutes (instance resize) Instant (on-demand mode)
Complex queries / JOINs Excellent Excellent Very limited (single-table design needed)
Cost at scale (10K+ RPS) Moderate–high Predictable (reserved) Low per-request
Ideal use case Variable-load apps, SaaS multi-tenant, APIs Stable OLTP, legacy migrations Session data, IoT, gaming leaderboards, high-velocity writes
Decision rule: If your application uses JOINs, stored procedures, transactions across multiple tables, or existing SQL codebases — Aurora Serverless v2. If you have a NoSQL access pattern with single-key lookups at massive scale — DynamoDB. If you need a stable, predictable workload with Reserved Instance savings — RDS provisioned.

Setting Up Aurora Serverless v2

You can create an Aurora Serverless v2 cluster via the AWS Console, AWS CLI, or CloudFormation/CDK. The key parameters are the engine version, the minimum and maximum ACU values, and whether to enable the Data API.

AWS CLI: Create an Aurora Serverless v2 PostgreSQL Cluster

# Step 1: Create the DB cluster (serverless v2 uses --engine-mode provisioned with ServerlessV2ScalingConfiguration)
aws rds create-db-cluster \
  --db-cluster-identifier my-aurora-sv2 \
  --engine aurora-postgresql \
  --engine-version 15.4 \
  --master-username dbadmin \
  --master-user-password "S3cure#Pass2026!" \
  --serverless-v2-scaling-configuration MinCapacity=0.5,MaxCapacity=16 \
  --enable-http-endpoint \
  --vpc-security-group-ids sg-0abc12345 \
  --db-subnet-group-name my-db-subnet-group \
  --backup-retention-period 7 \
  --storage-encrypted \
  --region us-east-1

# Step 2: Add the writer instance (db.serverless is the instance class for v2)
aws rds create-db-instance \
  --db-instance-identifier my-aurora-sv2-writer \
  --db-cluster-identifier my-aurora-sv2 \
  --db-instance-class db.serverless \
  --engine aurora-postgresql \
  --region us-east-1

# Step 3: Add a reader instance for read scaling
aws rds create-db-instance \
  --db-instance-identifier my-aurora-sv2-reader \
  --db-cluster-identifier my-aurora-sv2 \
  --db-instance-class db.serverless \
  --engine aurora-postgresql \
  --region us-east-1

# Verify cluster status
aws rds describe-db-clusters \
  --db-cluster-identifier my-aurora-sv2 \
  --query 'DBClusters[0].{Status:Status,Endpoint:Endpoint,ReaderEndpoint:ReaderEndpoint,MinACU:ServerlessV2ScalingConfiguration.MinCapacity,MaxACU:ServerlessV2ScalingConfiguration.MaxCapacity}'

CloudFormation YAML: Aurora Serverless v2 Cluster

AWSTemplateFormatVersion: '2010-09-09'
Description: Aurora Serverless v2 PostgreSQL Cluster

Parameters:
  DBMasterPassword:
    Type: String
    NoEcho: true
    Description: Master password for Aurora cluster

Resources:
  AuroraCluster:
    Type: AWS::RDS::DBCluster
    Properties:
      DBClusterIdentifier: aurora-sv2-prod
      Engine: aurora-postgresql
      EngineVersion: "15.4"
      MasterUsername: dbadmin
      MasterUserPassword: !Ref DBMasterPassword
      ServerlessV2ScalingConfiguration:
        MinCapacity: 0.5
        MaxCapacity: 32
      EnableHttpEndpoint: true
      StorageEncrypted: true
      BackupRetentionPeriod: 7
      DeletionProtection: true
      VpcSecurityGroupIds:
        - !Ref DBSecurityGroup
      DBSubnetGroupName: !Ref DBSubnetGroup
      Tags:
        - Key: Environment
          Value: production

  AuroraWriterInstance:
    Type: AWS::RDS::DBInstance
    Properties:
      DBInstanceIdentifier: aurora-sv2-prod-writer
      DBClusterIdentifier: !Ref AuroraCluster
      DBInstanceClass: db.serverless
      Engine: aurora-postgresql
      PubliclyAccessible: false
      EnablePerformanceInsights: true
      PerformanceInsightsRetentionPeriod: 7

  AuroraReaderInstance:
    Type: AWS::RDS::DBInstance
    Properties:
      DBInstanceIdentifier: aurora-sv2-prod-reader
      DBClusterIdentifier: !Ref AuroraCluster
      DBInstanceClass: db.serverless
      Engine: aurora-postgresql
      PubliclyAccessible: false

  DBSubnetGroup:
    Type: AWS::RDS::DBSubnetGroup
    Properties:
      DBSubnetGroupDescription: Aurora subnet group
      SubnetIds:
        - !Ref PrivateSubnet1
        - !Ref PrivateSubnet2

  DBSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Aurora DB Security Group
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 5432
          ToPort: 5432
          SourceSecurityGroupId: !Ref AppSecurityGroup

Outputs:
  ClusterEndpoint:
    Value: !GetAtt AuroraCluster.Endpoint.Address
  ReaderEndpoint:
    Value: !GetAtt AuroraCluster.ReadEndpoint.Address
  ClusterResourceId:
    Value: !GetAtt AuroraCluster.DBClusterResourceId
Min ACU 0 vs 0.5: Setting MinCapacity to 0 is only valid for Aurora Serverless v1. Aurora Serverless v2 requires a minimum of 0.5 ACU. The cluster always has at least 0.5 ACU running — there is no pause/resume cycle.

Aurora Data API

The Aurora Data API lets you run SQL statements over HTTPS without a persistent database connection. This is transformative for serverless Lambda functions, which can't maintain a TCP connection pool between cold-start invocations. You authenticate via IAM, pass the cluster ARN and a secret ARN (from AWS Secrets Manager), and get JSON results back.

The Data API is enabled by the --enable-http-endpoint flag on the cluster and is available for both Aurora MySQL and Aurora PostgreSQL (Serverless v2 and provisioned clusters in supported regions).

Python boto3: Aurora Data API execute_statement

import boto3
import json

# Aurora Data API client
rds_data = boto3.client('rds-data', region_name='us-east-1')

CLUSTER_ARN = 'arn:aws:rds:us-east-1:123456789012:cluster:my-aurora-sv2'
SECRET_ARN = 'arn:aws:secretsmanager:us-east-1:123456789012:secret:aurora/dbadmin-abc123'
DATABASE = 'myappdb'

def get_user(user_id: int) -> dict:
    """Fetch a single user row using the Data API."""
    response = rds_data.execute_statement(
        resourceArn=CLUSTER_ARN,
        secretArn=SECRET_ARN,
        database=DATABASE,
        sql='SELECT id, email, created_at FROM users WHERE id = :user_id',
        parameters=[
            {'name': 'user_id', 'value': {'longValue': user_id}}
        ]
    )
    records = response.get('records', [])
    if not records:
        return None
    row = records[0]
    return {
        'id': row[0]['longValue'],
        'email': row[1]['stringValue'],
        'created_at': row[2]['stringValue']
    }

def insert_order(user_id: int, product_id: int, quantity: int) -> str:
    """Insert an order row, return the generated order ID."""
    response = rds_data.execute_statement(
        resourceArn=CLUSTER_ARN,
        secretArn=SECRET_ARN,
        database=DATABASE,
        sql='''
            INSERT INTO orders (user_id, product_id, quantity, status)
            VALUES (:user_id, :product_id, :quantity, 'pending')
            RETURNING id
        ''',
        parameters=[
            {'name': 'user_id',    'value': {'longValue': user_id}},
            {'name': 'product_id', 'value': {'longValue': product_id}},
            {'name': 'quantity',   'value': {'longValue': quantity}}
        ]
    )
    return response['records'][0][0]['stringValue']

def batch_insert_items(items: list) -> int:
    """Batch insert using execute_batch_statement for efficiency."""
    param_sets = [
        [
            {'name': 'name',  'value': {'stringValue': item['name']}},
            {'name': 'price', 'value': {'doubleValue': item['price']}},
            {'name': 'stock', 'value': {'longValue': item['stock']}}
        ]
        for item in items
    ]
    response = rds_data.batch_execute_statement(
        resourceArn=CLUSTER_ARN,
        secretArn=SECRET_ARN,
        database=DATABASE,
        sql='INSERT INTO products (name, price, stock) VALUES (:name, :price, :stock)',
        parameterSets=param_sets
    )
    return len(response['updateResults'])

# Lambda handler example
def lambda_handler(event, context):
    user_id = int(event.get('userId', 1))
    user = get_user(user_id)
    if not user:
        return {'statusCode': 404, 'body': json.dumps({'error': 'User not found'})}
    return {'statusCode': 200, 'body': json.dumps(user)}
Data API Limits: Maximum request size is 1 MB. Maximum response size is 1 MB. Maximum result set is 1,000 rows. For large result sets, use pagination or a persistent connection via RDS Proxy instead of the Data API.

Lambda Function Using Data API (Full Example)

import boto3
import json
import os

rds_data = boto3.client('rds-data')

CLUSTER_ARN = os.environ['AURORA_CLUSTER_ARN']
SECRET_ARN  = os.environ['AURORA_SECRET_ARN']
DB_NAME     = os.environ.get('DB_NAME', 'appdb')

def run_sql(sql: str, params: list = None, transaction_id: str = None) -> dict:
    """Helper to run SQL via the Data API."""
    kwargs = {
        'resourceArn': CLUSTER_ARN,
        'secretArn': SECRET_ARN,
        'database': DB_NAME,
        'sql': sql,
    }
    if params:
        kwargs['parameters'] = params
    if transaction_id:
        kwargs['transactionId'] = transaction_id
    return rds_data.execute_statement(**kwargs)

def lambda_handler(event, context):
    action = event.get('action')

    if action == 'create_product':
        # Start a transaction
        tx = rds_data.begin_transaction(
            resourceArn=CLUSTER_ARN,
            secretArn=SECRET_ARN,
            database=DB_NAME
        )
        tx_id = tx['transactionId']
        try:
            run_sql(
                'INSERT INTO products (name, price) VALUES (:name, :price)',
                [
                    {'name': 'name',  'value': {'stringValue': event['name']}},
                    {'name': 'price', 'value': {'doubleValue': float(event['price'])}}
                ],
                transaction_id=tx_id
            )
            run_sql(
                'INSERT INTO audit_log (action, entity) VALUES (:action, :entity)',
                [
                    {'name': 'action', 'value': {'stringValue': 'CREATE_PRODUCT'}},
                    {'name': 'entity', 'value': {'stringValue': event['name']}}
                ],
                transaction_id=tx_id
            )
            rds_data.commit_transaction(
                resourceArn=CLUSTER_ARN,
                secretArn=SECRET_ARN,
                transactionId=tx_id
            )
            return {'statusCode': 201, 'body': json.dumps({'message': 'Product created'})}
        except Exception as e:
            rds_data.rollback_transaction(
                resourceArn=CLUSTER_ARN,
                secretArn=SECRET_ARN,
                transactionId=tx_id
            )
            return {'statusCode': 500, 'body': json.dumps({'error': str(e)})}

Connection Pooling with RDS Proxy

While the Data API handles connectionless queries elegantly, many applications use database connection libraries (SQLAlchemy, Hibernate, pg, mysql2) that require a persistent TCP connection. In a Lambda or container environment with hundreds of concurrent instances, each opening its own connection can exhaust Aurora's max connection count — typically 1,000–5,000 depending on ACU size.

RDS Proxy solves this by maintaining a pool of long-lived connections to Aurora and multiplexing hundreds of application connections onto them. It also supports connection pinning (for transactions), IAM authentication, and automatic failover handling.

RDS Proxy Setup via AWS CLI

# Create the RDS Proxy
aws rds create-db-proxy \
  --db-proxy-name aurora-sv2-proxy \
  --engine-family POSTGRESQL \
  --auth '[{
    "AuthScheme": "SECRETS",
    "SecretArn": "arn:aws:secretsmanager:us-east-1:123456789012:secret:aurora/dbadmin-abc123",
    "IAMAuth": "REQUIRED"
  }]' \
  --role-arn arn:aws:iam::123456789012:role/rds-proxy-role \
  --vpc-subnet-ids subnet-0abc1 subnet-0abc2 \
  --vpc-security-group-ids sg-0abc12345 \
  --require-tls \
  --idle-client-timeout 1800 \
  --region us-east-1

# Register the proxy target (point to our Aurora cluster)
aws rds register-db-proxy-targets \
  --db-proxy-name aurora-sv2-proxy \
  --db-cluster-identifiers my-aurora-sv2 \
  --region us-east-1

# Get the proxy endpoint for your connection string
aws rds describe-db-proxies \
  --db-proxy-name aurora-sv2-proxy \
  --query 'DBProxies[0].Endpoint'

With RDS Proxy in place, your Lambda functions connect to the proxy endpoint instead of the Aurora cluster endpoint. The proxy handles pooling, and your connection string looks like:

postgresql://dbadmin@aurora-sv2-proxy.proxy-abc123.us-east-1.rds.amazonaws.com:5432/appdb
IAM Authentication with RDS Proxy: When IAMAuth: REQUIRED is set, Lambda functions must generate a short-lived RDS authentication token using rds.generate_db_auth_token() instead of a static password. This eliminates hardcoded credentials entirely. The Lambda execution role must have the rds-db:connect permission.

Multi-AZ and Global Database

Aurora Serverless v2 supports Multi-AZ deployments by placing reader instances in different Availability Zones. If the writer instance fails, Aurora promotes a reader to writer in under 30 seconds with automatic DNS failover via the cluster endpoint. Your application continues using the same endpoint — no manual intervention needed.

Aurora Global Database extends this across AWS regions. A primary region handles all writes. Up to 5 secondary regions receive replicated data with typically under 1 second of replication lag. In the event of a regional failure, you can promote a secondary to primary (planned or unplanned failover). Aurora Serverless v2 writer and reader instances in each region independently scale their ACU capacity based on local read traffic.

Enabling Multi-AZ with an Additional Reader

# Add a reader in a different AZ (Aurora places it automatically for AZ diversity)
aws rds create-db-instance \
  --db-instance-identifier my-aurora-sv2-reader-az2 \
  --db-cluster-identifier my-aurora-sv2 \
  --db-instance-class db.serverless \
  --engine aurora-postgresql \
  --availability-zone us-east-1b \
  --region us-east-1

# Check AZ placement of all instances
aws rds describe-db-instances \
  --filters Name=db-cluster-id,Values=my-aurora-sv2 \
  --query 'DBInstances[*].{ID:DBInstanceIdentifier,AZ:AvailabilityZone,Status:DBInstanceStatus}'

Cost Model: ACU-Hours, Storage, and I/O

Aurora Serverless v2 billing has three components:

  • ACU-hours: You pay per ACU per hour for each instance (writer + readers). In us-east-1, Aurora PostgreSQL Serverless v2 costs approximately $0.12 per ACU-hour (2026 pricing — check the AWS pricing page for your region).
  • Storage: $0.10 per GB-month for Aurora storage (billed in 10 GB increments, automatically grows).
  • I/O: $0.20 per million I/O requests (for Aurora Standard storage). Aurora I/O-Optimized storage eliminates per-I/O charges for ~25% higher storage cost — better for I/O-heavy workloads.

Worked Cost Example: Variable Workload

Consider an e-commerce application with the following daily pattern:

  • Night (10 PM – 8 AM): minimal traffic, ~0.5 ACU for 10 hours
  • Business hours (8 AM – 6 PM): moderate traffic, ~4 ACU for 10 hours
  • Peak (6 PM – 10 PM): heavy traffic, ~12 ACU for 4 hours
Daily ACU-hours (writer only):
  Night:    0.5 ACU × 10h = 5 ACU-h
  Business: 4.0 ACU × 10h = 40 ACU-h
  Peak:    12.0 ACU ×  4h = 48 ACU-h
  Total:                    93 ACU-h/day

Monthly cost (30 days):
  ACU cost: 93 × 30 × $0.12 = $334.80/month

Equivalent provisioned Aurora (db.r6g.xlarge = 4 vCPU, 32 GB, ~16 ACU):
  On-Demand: $0.48/hour × 730h = $350.40/month
  Reserved 1yr: ~$220/month (no unused capacity savings, but fixed)

Storage (200 GB): 200 × $0.10 = $20/month
I/O (500M requests): 500 × $0.20 = $100/month

Total Aurora Serverless v2: ~$454.80/month
Total Aurora Provisioned (On-Demand): ~$470.40/month (and you paid for idle ACUs)

Savings from Serverless v2: ~3% with this pattern.
BUT: during low-traffic months or early-stage products, savings reach 40–70%.
When Provisioned Beats Serverless v2: If your workload runs at consistently high ACU (near maximum) 24/7, provisioned Aurora with Reserved Instances is cheaper. Serverless v2 pays off most when traffic is variable, bursty, or unpredictable — including dev/staging environments running part-time.

Zero-ETL Integration with Redshift

AWS Zero-ETL for Aurora and Amazon Redshift, now generally available, allows near-real-time data replication from an Aurora MySQL cluster to a Redshift data warehouse — with no ETL code, no intermediate S3 staging, and no DMS pipeline to manage. Changes in Aurora appear in Redshift within seconds.

Aurora Serverless v2 supports Zero-ETL (currently with Aurora MySQL 3.05+ and Redshift ra3 node types). You create a Zero-ETL integration from the RDS Console or CLI, select the source Aurora cluster and target Redshift namespace, and AWS handles the replication internally using Aurora's binlog stream.

Creating a Zero-ETL Integration

# Enable binlog replication on the Aurora cluster parameter group first
aws rds create-db-cluster-parameter-group \
  --db-cluster-parameter-group-name aurora-zeroetl-params \
  --db-parameter-group-family aurora-mysql8.0 \
  --description "Zero-ETL enabled parameter group"

aws rds modify-db-cluster-parameter-group \
  --db-cluster-parameter-group-name aurora-zeroetl-params \
  --parameters \
    "ParameterName=binlog_format,ParameterValue=ROW,ApplyMethod=pending-reboot" \
    "ParameterName=binlog_row_image,ParameterValue=full,ApplyMethod=pending-reboot"

# Apply the parameter group to your cluster
aws rds modify-db-cluster \
  --db-cluster-identifier my-aurora-sv2 \
  --db-cluster-parameter-group-name aurora-zeroetl-params \
  --apply-immediately

# Create the Zero-ETL integration (Redshift namespace ARN required)
aws rds create-integration \
  --integration-name aurora-to-redshift \
  --source-arn arn:aws:rds:us-east-1:123456789012:cluster:my-aurora-sv2 \
  --target-arn arn:aws:redshift-serverless:us-east-1:123456789012:namespace/my-namespace \
  --kms-key-id arn:aws:kms:us-east-1:123456789012:key/mrk-abc123 \
  --region us-east-1

# Monitor replication status
aws rds describe-integrations \
  --integration-name aurora-to-redshift \
  --query 'Integrations[0].{Status:Status,Tables:DataFilter}'

Once the integration is active, Redshift creates a database mirroring the Aurora schema. You query it with standard Redshift SQL — no COPY commands, no Glue jobs, no Airflow DAGs. This eliminates the typical 4–8 hour latency of traditional nightly ETL pipelines.

Migration from RDS MySQL/PostgreSQL to Aurora

Migrating from RDS to Aurora is simpler than migrating between different database engines because Aurora is binary-compatible with MySQL 8.0 and PostgreSQL. AWS provides three main migration paths:

Option 1: Snapshot Migration (Minimal Downtime)

Take a snapshot of your RDS instance, restore it as an Aurora cluster. This causes a downtime window equal to the restore time (typically minutes to an hour depending on database size).

# Step 1: Take a final snapshot of your RDS instance
aws rds create-db-snapshot \
  --db-instance-identifier my-rds-postgres \
  --db-snapshot-identifier my-rds-final-snapshot \
  --region us-east-1

# Wait for snapshot to complete
aws rds wait db-snapshot-available \
  --db-snapshot-identifier my-rds-final-snapshot

# Step 2: Restore the snapshot as an Aurora cluster
aws rds restore-db-cluster-from-snapshot \
  --db-cluster-identifier my-aurora-sv2-migrated \
  --snapshot-identifier my-rds-final-snapshot \
  --engine aurora-postgresql \
  --engine-version 15.4 \
  --serverless-v2-scaling-configuration MinCapacity=0.5,MaxCapacity=16 \
  --enable-http-endpoint \
  --region us-east-1

# Step 3: Add the writer instance
aws rds create-db-instance \
  --db-instance-identifier my-aurora-sv2-migrated-writer \
  --db-cluster-identifier my-aurora-sv2-migrated \
  --db-instance-class db.serverless \
  --engine aurora-postgresql

Option 2: AWS DMS for Near-Zero Downtime

For production databases where downtime must be minimized to seconds, use AWS Database Migration Service (DMS) with Change Data Capture (CDC). DMS performs a full load to Aurora, then captures and replays all changes from the RDS transaction log while your application continues running against RDS. When replication lag reaches near-zero, you do a brief cutover (update connection strings, flip DNS).

Key DMS settings for PostgreSQL CDC:

  • Source: set wal_level = logical on the RDS parameter group and create a replication slot.
  • Target: Aurora PostgreSQL with IAM credentials or Secrets Manager integration.
  • Task type: full-load-and-cdc.
  • Table mappings: include all schemas or specify per-schema include rules.

Option 3: pg_dump / mysqldump for Small Databases

For databases under 50 GB, a simple dump-and-restore via pg_dump (PostgreSQL) or mysqldump (MySQL) is the most straightforward approach. Export from RDS, import to Aurora during a maintenance window. Connection strings change only in endpoint hostname — the port, username, database name, and schema remain identical.

Post-Migration Checklist: After migrating to Aurora, update application connection strings to use the Aurora cluster endpoint (not reader endpoint for writes). Re-test your connection pool configuration — Aurora's max_connections is ACU-dependent (roughly 90 connections per ACU for PostgreSQL). If you have many Lambda functions, set up RDS Proxy to avoid connection exhaustion. Validate Performance Insights is enabled to catch any query regressions.

Frequently Asked Questions

Does Aurora Serverless v2 scale to zero?

No. Aurora Serverless v2 scales down to a minimum of 0.5 ACU but never pauses. This eliminates cold starts (v1 could take 20–30 seconds to resume from a paused state). If scale-to-zero is a hard requirement and you can tolerate cold starts, Aurora Serverless v1 is still available, but it is approaching end-of-life and should not be used for new applications.

What is the maximum ACU for Aurora Serverless v2?

Per writer or reader instance, the maximum is 128 ACU (approximately 256 GB RAM). For higher capacity, add more reader instances and distribute read load — Aurora Serverless v2 clusters can have up to 15 reader instances, each scaling independently to 128 ACU.

Can I use Aurora Serverless v2 with a VPC Lambda?

Yes. The most common architecture is: Lambda in VPC → RDS Proxy → Aurora Serverless v2. The proxy handles connection pooling so Lambda cold starts don't flood Aurora with new connections. Alternatively, if your Lambda is outside a VPC, use the Aurora Data API over HTTPS — no VPC configuration required.

How do I monitor ACU usage?

CloudWatch metrics for Aurora Serverless v2 include ServerlessDatabaseCapacity (current ACU), ACUUtilization (percentage of max ACU in use), DatabaseConnections, and standard Aurora metrics like WriteLatency and CPUUtilization. Set CloudWatch alarms on ACUUtilization approaching 100% to proactively increase MaxCapacity.

Is Aurora Serverless v2 HIPAA and PCI compliant?

Yes. Aurora Serverless v2 is included in the AWS HIPAA BAA, PCI DSS, SOC 1/2/3, and ISO 27001 compliance programs. Enable storage encryption (KMS), TLS in transit, CloudTrail logging, and VPC isolation to meet most regulatory requirements.

What happens during a scale-up event?

Aurora Serverless v2 scales by adding compute resources to the existing instance without restarting it. Active connections are preserved. There is no brief pause like in v1. From the application's perspective, queries may take slightly longer during the scale-up moment (a few milliseconds), but connections do not drop.

Read Next

AWS RDS Database Guide

Deep dive into RDS provisioned instances, Multi-AZ, Read Replicas, and parameter groups.

AWS Lambda Serverless Guide

Lambda functions, event sources, layers, and connecting Lambda to Aurora via Data API or RDS Proxy.