Back to Blog
AWSAuroraServerlessMySQL

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.

Azynth Team
13 min read

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:

  1. Aurora monitors CPU, memory, and connections
  2. Scales ACUs up/down in 0.5 ACU increments based on workload
  3. No pause in processing - happens while SQL statements run
  4. Zero connection disruption - transactions remain open during scaling
  5. 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:

  1. max_connections is static: The parameter is set based on your maximum configured ACUs, not current ACUs
  2. Prevents resource waste: Without pooling, apps open too many connections even when ACUs are low
  3. 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