AWS Aurora Serverless V2: MySQL That Scales to Zero
Master Aurora Serverless V2 for auto-scaling MySQL: ACU management, cost optimization, connection pooling, and when to use serverless over provisioned.
AWS Aurora Serverless V2: MySQL That Scales to Zero
Aurora Serverless V2 is a game-changer for variable workloads. Here's how to leverage auto-scaling MySQL that adjusts capacity in real-time and can scale down to nearly zero when idle.
What is Aurora Serverless V2?
Unlike provisioned Aurora where you choose instance sizes (db.r5.large, etc.), Serverless V2 uses Aurora Capacity Units (ACUs) that scale automatically:
Provisioned Aurora:
- Fixed instance size (e.g., db.r5.2xlarge)
- Manual scaling requires downtime
- Pay for capacity even when idle
- Minimum cost: ~$300/month per instance
Aurora Serverless V2:
- Auto-scales from 0.5 to 128 ACUs (1 ACU = 2GB RAM)
- Scales in <1 second granularly
- Pay only for capacity used (per second billing)
- Can scale to near-zero during idle periods
The ACU Scaling Model
resource "aws_rds_cluster" "serverless" { cluster_identifier = "production-db" engine = "aurora-mysql" engine_version = "8.0.mysql_aurora.3.04.0" engine_mode = "provisioned" # Yes, "provisioned" for Serverless V2 database_name = "mydb" master_username = "admin" serverlessv2_scaling_configuration { min_capacity = 0.5 # Minimum: 1 GB RAM max_capacity = 8 # Maximum: 16 GB RAM } } resource "aws_rds_cluster_instance" "serverless" { count = 2 # Writer + 1 reader cluster_identifier = aws_rds_cluster.serverless.id instance_class = "db.serverless" # Special serverless instance class engine = "aurora-mysql" }
How it works:
- Aurora monitors CPU, memory, and connections
- Scales ACUs up/down in 0.5 ACU increments based on workload
- No pause in processing - happens while SQL statements run
- Zero connection disruption - transactions remain open during scaling
- Scaling typically completes within seconds
Cost Comparison
Scenario: E-commerce site with variable traffic
Provisioned Aurora (db.r5.large):
- 2 vCPU, 16 GB RAM
- $0.29/hour × 730 hours = $212/month
- Cost is fixed even at night when traffic is 10% of peak
Serverless V2 (0.5-4 ACUs):
- $0.12 per ACU-hour for MySQL
- Peak: 4 ACUs × 8 hours × 30 days × $0.12 = $115
- Normal: 2 ACUs × 12 hours × 30 days × $0.12 = $86
- Idle: 0.5 ACUs × 4 hours × 30 days × $0.12 = $7
- Total: $208/month (but only paying for what you use)
For truly idle periods (dev/staging), savings can be 80%+.
Connection Management: Why It Still Matters
While Aurora Serverless V2 scales capacity quickly, connection pooling is still critical because:
max_connectionsis static: The parameter is set based on your maximum configured ACUs, not current ACUs- Prevents resource waste: Without pooling, apps open too many connections even when ACUs are low
- AWS recommendation: Amazon explicitly recommends using RDS Proxy for Serverless V2
Example: If your max ACU is 16 and min is 0.5:
max_connections= 2,000 (static, based on max)- When scaled down to 0.5 ACUs, you still allow 2,000 connections
- Each idle connection consumes memory → forces unnecessary scaling
Solution: Use RDS Proxy
RDS Proxy maintains a pool of connections and multiplexes application requests:
resource "aws_db_proxy" "serverless" { name = "serverless-proxy" engine_family = "MYSQL" auth { auth_scheme = "SECRETS" iam_auth = "DISABLED" secret_arn = aws_secretsmanager_secret.db_credentials.arn } role_arn = aws_iam_role.proxy.arn vpc_subnet_ids = module.vpc.private_subnets # Connection pooling configuration require_tls = true }
Read Scaling with Serverless
Unlike provisioned Aurora, each Serverless V2 replica scales independently:
resource "aws_rds_cluster_instance" "serverless_writer" { cluster_identifier = aws_rds_cluster.serverless.id instance_class = "db.serverless" engine = "aurora-mysql" # Writer can scale 0.5-16 ACUs } resource "aws_rds_cluster_instance" "serverless_reader_1" { cluster_identifier = aws_rds_cluster.serverless.id instance_class = "db.serverless" engine = "aurora-mysql" # Reader can have different scaling config # Scales independently based on read load } resource "aws_rds_cluster_instance" "serverless_reader_2" { cluster_identifier = aws_rds_cluster.serverless.id instance_class = "db.serverless" engine = "aurora-mysql" }
Pro tip: Use reader endpoint for reads:
import mysql from 'mysql2/promise'; const writerPool = mysql.createPool({ host: 'serverless-cluster.cluster-xxx.rds.amazonaws.com', database: 'mydb', waitForConnections: true, connectionLimit: 10 }); const readerPool = mysql.createPool({ host: 'serverless-cluster.cluster-ro-xxx.rds.amazonaws.com', database: 'mydb', waitForConnections: true, connectionLimit: 50 // More connections for reads }); // Route writes to writer async function createUser(email: string) { const [result] = await writerPool.execute( 'INSERT INTO users (email) VALUES (?)', [email] ); return result; } // Route reads to reader async function getUsers() { const [rows] = await readerPool.execute('SELECT * FROM users WHERE active = 1'); return rows; }
Auto-Scaling Replicas
Add/remove read replicas based on load:
resource "aws_appautoscaling_target" "serverless_replicas" { service_namespace = "rds" scalable_dimension = "rds:cluster:ReadReplicaCount" resource_id = "cluster:${aws_rds_cluster.serverless.id}" min_capacity = 1 max_capacity = 5 } resource "aws_appautoscaling_policy" "serverless_replicas" { name = "serverless-reader-scaling" service_namespace = aws_appautoscaling_target.serverless_replicas.service_namespace scalable_dimension = aws_appautoscaling_target.serverless_replicas.scalable_dimension resource_id = aws_appautoscaling_target.serverless_replicas.resource_id policy_type = "TargetTrackingScaling" target_tracking_scaling_policy_configuration { predefined_metric_specification { predefined_metric_type = "RDSReaderAverageCPUUtilization" } target_value = 60.0 # Lower for serverless (more responsive) } }
Monitoring Serverless Capacity
Critical metrics to track:
# CloudWatch alarms for Serverless V2 alarms: - name: HighACUUsage metric: ServerlessDatabaseCapacity statistic: Average threshold: 7.5 # 93% of max (8 ACUs) evaluation_periods: 2 alarm_description: "Serverless approaching max capacity" - name: FrequentScaling metric: ServerlessDatabaseCapacity statistic: SampleCount threshold: 100 # Too many scaling events evaluation_periods: 1 period: 300 - name: ConnectionsNearMax metric: DatabaseConnections threshold: 1000 # Varies by ACU count
Best Practices
1. Set Appropriate Min/Max ACUs
Don't set min too low or max too high:
# BAD: Min too low for production serverlessv2_scaling_configuration { min_capacity = 0.5 # Production workload will constantly scale up max_capacity = 64 } # GOOD: Set min to baseline load serverlessv2_scaling_configuration { min_capacity = 2 # Baseline for production traffic max_capacity = 16 # Reasonable ceiling }
2. Use for Development/Staging
Perfect use case for Serverless V2:
# Dev environment - aggressive scaling down resource "aws_rds_cluster" "dev" { cluster_identifier = "dev-db" engine = "aurora-mysql" serverlessv2_scaling_configuration { min_capacity = 0.5 # Scale to minimum when not in use max_capacity = 4 } } # Production - higher baseline resource "aws_rds_cluster" "prod" { cluster_identifier = "prod-db" engine = "aurora-mysql" serverlessv2_scaling_configuration { min_capacity = 4 # Always ready for traffic max_capacity = 32 } }
3. Implement Connection Pooling
Critical for Serverless:
// PHP: Reuse connections across requests class Database { private static $pdo = null; public static function getConnection() { if (self::$pdo === null) { self::$pdo = new PDO( 'mysql:host=serverless-cluster.rds.amazonaws.com;dbname=mydb', 'admin', getenv('DB_PASSWORD'), [ PDO::ATTR_PERSISTENT => true, // Set to false if using RDS Proxy PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION ] ); } return self::$pdo; } }
When to Use Serverless V2
Excellent for:
- Development/staging environments (scale to near-zero)
- Batch processing workloads (scale up during job, down after)
- Variable traffic patterns (e-commerce, news sites)
- Microservices with intermittent database access
- Multi-tenant SaaS with unpredictable tenant usage
Not ideal for:
- Consistently high load (provisioned is cheaper)
- Sub-second latency requirements (scaling adds tiny overhead)
- Legacy apps with connection leaks (will scale infinitely)
- Workloads requiring >128 ACUs (provisioned goes higher)
Migration from Provisioned Aurora
Zero-downtime migration:
# 1. Create Serverless V2 cluster from snapshot aws rds restore-db-cluster-to-point-in-time \ --source-db-cluster-identifier provisioned-cluster \ --target-db-cluster-identifier serverless-cluster \ --engine-mode provisioned \ --serverless-v2-scaling-configuration MinCapacity=2,MaxCapacity=16 # 2. Add serverless instances aws rds create-db-instance \ --db-instance-identifier serverless-cluster-instance-1 \ --db-cluster-identifier serverless-cluster \ --db-instance-class db.serverless \ --engine aurora-mysql # 3. Update application connection string # 4. Delete provisioned cluster
Cost Optimization Tips
1. Use Scheduled Scaling for Predictable Patterns
import boto3 import schedule import time rds = boto3.client('rds') def scale_down(): """Scale down during off-hours""" rds.modify_current_db_cluster_capacity( DBClusterIdentifier='serverless-cluster', Capacity=1 # 2 GB RAM ) def scale_up(): """Scale up before peak hours""" rds.modify_current_db_cluster_capacity( DBClusterIdentifier='serverless-cluster', Capacity=8 # 16 GB RAM ) # Schedule scaling schedule.every().day.at("22:00").do(scale_down) # 10 PM schedule.every().day.at("06:00").do(scale_up) # 6 AM while True: schedule.run_pending() time.sleep(60)
2. Monitor and Adjust Limits
Review actual usage monthly:
-- Query to see connection patterns SELECT DATE_FORMAT(timestamp, '%Y-%m-%d %H:00:00') as hour, AVG(connections) as avg_connections, MAX(connections) as peak_connections FROM mysql.rds_history WHERE timestamp > DATE_SUB(NOW(), INTERVAL 30 DAY) GROUP BY hour ORDER BY hour DESC;
Adjust ACU limits based on real usage.
Conclusion
Aurora Serverless V2 is perfect for workloads with variable or unpredictable traffic. With sub-second scaling and per-second billing, you pay only for what you use while maintaining enterprise-grade MySQL performance.
Key benefits:
- Scale from 0.5 to 128 ACUs automatically
- Pay per second (no idle waste)
- <1 second scaling response
- Full Aurora reliability and features
Trade-offs:
- Slightly higher per-ACU cost than provisioned
- Connection management requires more care
- Not ideal for steady 24/7 high load
Need help with Aurora Serverless V2 architecture? Let's talk about your scaling requirements.
You might also like
AWS Cost Optimization: The Ultimate Guide
Slash your AWS bill with these proven strategies: right-sizing, savings plans, spot instances, and more.
AWS ECS Production Deployment: The Complete Guide
Deploy containerized applications on AWS ECS with auto-scaling, blue/green deployments, and production-grade monitoring.
AWS VPC Deep Dive: Production Networking That Scales
Master AWS VPC networking for production: subnets, route tables, NAT gateways, security groups, and network architecture patterns that scale securely.