Amazon Aurora Serverless v2: On-Demand Database Scaling Without the Guesswork

Provisioning RDS instances means picking an instance size and living with it. Too small and you throttle under load. Too large and you pay for capacity that sits idle most of the day. For workloads with unpredictable or highly variable traffic, neither option is good.

Aurora Serverless v2 removes that decision. It scales your database compute up and down in fine-grained increments based on actual demand, in seconds, without dropping connections. You set a minimum and maximum capacity range, and Aurora handles everything in between.

In this article I will walk through how Aurora Serverless v2 works, when it makes sense to use it, the limitations you need to know, and how to configure it correctly with Terraform.

How Aurora Serverless v2 Scales

Aurora Serverless v2 capacity is measured in Aurora Capacity Units, or ACUs. Each ACU represents approximately 2GB of memory along with proportional CPU and network. You configure a minimum ACU value and a maximum ACU value. The database scales between those bounds based on load.

Scaling happens in increments as small as 0.5 ACU and takes effect within a few seconds. Scaling up does not drop connections. Scaling down waits for a quiet moment with no active transactions before reducing capacity, so your application never sees an interruption from a scale-down event.

You are billed per ACU-hour for the capacity you actually use, rounded to the nearest second. At minimum capacity, a serverless instance with a minimum of 0.5 ACU costs significantly less than the smallest provisioned Aurora instance. At peak, it can scale to the equivalent of a very large provisioned instance.

When to Use It and When Not To

Aurora Serverless v2 is the right choice when your workload has variable traffic patterns: development and staging environments that are idle overnight, applications with distinct peak hours, SaaS platforms where different customer tiers have different usage patterns, and any workload where you genuinely cannot predict peak load in advance.

It is not automatically the cheapest option. For workloads running at consistently high utilization around the clock, a provisioned Aurora instance with reserved capacity pricing will cost less. Run the math for your specific workload before choosing.

There are a few feature limitations to know. Aurora Serverless v2 does not support fast cloning from a serverless cluster to a provisioned cluster. It does not support Babelfish for Aurora PostgreSQL. Some instance-level parameter groups behave differently at low capacity. These are edge cases for most workloads but worth checking against your requirements.

Setting Up Aurora Serverless v2 with Terraform

resource "aws_rds_cluster" "main" {
cluster_identifier = "app-aurora-cluster"
engine = "aurora-postgresql"
engine_mode = "provisioned"
engine_version = "16.4"
database_name = "appdb"
master_username = "app_admin"
manage_master_user_password = true
master_user_secret_kms_key_id = aws_kms_key.rds.arn
serverlessv2_scaling_configuration {
min_capacity = 0.5
max_capacity = 16.0
seconds_until_auto_pause = 300
}
storage_encrypted = true
kms_key_id = aws_kms_key.rds.arn
vpc_security_group_ids = [aws_security_group.aurora.id]
db_subnet_group_name = aws_db_subnet_group.main.name
backup_retention_period = 14
preferred_backup_window = "03:00-04:00"
preferred_maintenance_window = "sun:05:00-sun:06:00"
deletion_protection = true
skip_final_snapshot = false
final_snapshot_identifier = "app-aurora-final-snapshot"
enabled_cloudwatch_logs_exports = ["postgresql"]
tags = {
Environment = "production"
ManagedBy = "terraform"
}
}
resource "aws_rds_cluster_instance" "writer" {
identifier = "app-aurora-writer"
cluster_identifier = aws_rds_cluster.main.id
instance_class = "db.serverless"
engine = aws_rds_cluster.main.engine
engine_version = aws_rds_cluster.main.engine_version
publicly_accessible = false
performance_insights_enabled = true
performance_insights_kms_key_id = aws_kms_key.rds.arn
performance_insights_retention_period = 7
monitoring_interval = 60
monitoring_role_arn = aws_iam_role.rds_enhanced_monitoring.arn
tags = { Role = "writer" }
}
resource "aws_rds_cluster_instance" "reader" {
identifier = "app-aurora-reader"
cluster_identifier = aws_rds_cluster.main.id
instance_class = "db.serverless"
engine = aws_rds_cluster.main.engine
engine_version = aws_rds_cluster.main.engine_version
publicly_accessible = false
performance_insights_enabled = true
performance_insights_kms_key_id = aws_kms_key.rds.arn
performance_insights_retention_period = 7
monitoring_interval = 60
monitoring_role_arn = aws_iam_role.rds_enhanced_monitoring.arn
tags = { Role = "reader" }
}
resource "aws_db_subnet_group" "main" {
name = "aurora-subnet-group"
subnet_ids = var.data_subnet_ids
tags = { Name = "aurora-subnet-group" }
}

engine_mode = “provisioned” is correct for Serverless v2. This is counterintuitive but it is how the API works. Aurora Serverless v1 used engine_mode = “serverless”. Serverless v2 is configured at the instance class level with db.serverless, not at the cluster engine mode.

manage_master_user_password = true tells Aurora to store the master password in Secrets Manager automatically and rotate it. This is the cleanest way to handle the initial credential without it ever appearing in Terraform state or your shell history.

seconds_until_auto_pause = 300 enables auto-pause after 5 minutes of inactivity. This is most useful for development and staging environments where the database should scale to zero when nobody is using it. For production, set this to 0 to disable auto-pause since the cold start on the first connection after a pause takes several seconds.

Capacity Planning

Setting the right minimum and maximum ACU values requires understanding your workload. Here is a practical approach.

For the minimum, think about your baseline idle state. A minimum of 0.5 ACU is suitable for development environments. For production applications with always-on traffic, set the minimum to at least 2 ACU to avoid the overhead of scaling from near-zero on every request spike.

For the maximum, look at your peak load requirements. 16 ACU is roughly equivalent to a db.r6g.xlarge provisioned instance. 64 ACU is roughly equivalent to a db.r6g.4xlarge. If you previously ran on a specific provisioned instance type, use that as your maximum ceiling and set it a bit higher to give room for unexpected spikes.

Watch the ServerlessDatabaseCapacity metric in CloudWatch after deployment. If it consistently hits your maximum, the maximum is too low. If it rarely exceeds a fraction of your maximum, you can lower the ceiling without risk.

Connecting Your Application

Aurora Serverless v2 clusters expose two endpoints: the writer endpoint and the reader endpoint. Use the writer endpoint for all write operations and the reader endpoint for read-heavy queries like reporting and analytics.

import boto3
import psycopg2
import json
import os
def get_db_credentials() -> dict:
client = boto3.client("secretsmanager", region_name=os.environ["AWS_REGION"])
secret_arn = os.environ["DB_SECRET_ARN"]
response = client.get_secret_value(SecretId=secret_arn)
return json.loads(response["SecretString"])
def get_writer_connection():
creds = get_db_credentials()
return psycopg2.connect(
host=os.environ["DB_WRITER_ENDPOINT"],
port=5432,
database=os.environ["DB_NAME"],
user=creds["username"],
password=creds["password"],
sslmode="require",
connect_timeout=10
)
def get_reader_connection():
creds = get_db_credentials()
return psycopg2.connect(
host=os.environ["DB_READER_ENDPOINT"],
port=5432,
database=os.environ["DB_NAME"],
user=creds["username"],
password=creds["password"],
sslmode="require",
connect_timeout=10
)

Pair Aurora Serverless v2 with RDS Proxy, which we covered in an earlier article, to handle connection pooling. Serverless instances scale capacity quickly but they still have connection limits based on their current ACU level. RDS Proxy absorbs connection spikes while the database is scaling up.

Monitoring Serverless Capacity

resource "aws_cloudwatch_metric_alarm" "capacity_at_max" {
alarm_name = "aurora-serverless-at-max-capacity"
comparison_operator = "GreaterThanOrEqualToThreshold"
evaluation_periods = 5
metric_name = "ServerlessDatabaseCapacity"
namespace = "AWS/RDS"
period = 60
statistic = "Maximum"
threshold = 14.0
alarm_description = "Aurora Serverless capacity near maximum. Consider raising the limit."
alarm_actions = [aws_sns_topic.alerts.arn]
dimensions = {
DBClusterIdentifier = aws_rds_cluster.main.cluster_identifier
}
}
resource "aws_cloudwatch_metric_alarm" "high_connections" {
alarm_name = "aurora-high-connection-count"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 3
metric_name = "DatabaseConnections"
namespace = "AWS/RDS"
period = 60
statistic = "Average"
threshold = 500
alarm_description = "Aurora connection count is high"
alarm_actions = [aws_sns_topic.alerts.arn]
dimensions = {
DBClusterIdentifier = aws_rds_cluster.main.cluster_identifier
}
}

The capacity alarm threshold is set to 14 against a maximum of 16. This gives you an early warning before the ceiling is hit so you can raise the maximum before load actually saturates the database.

Closing Thoughts

Aurora Serverless v2 is the right default for most new PostgreSQL and MySQL workloads on AWS unless you have strong evidence that your load is flat enough to benefit from reserved provisioned pricing. The operational simplicity of not managing instance sizes, combined with the cost efficiency during off-peak hours, makes it worth using even when the math is roughly equivalent to a provisioned instance.

Set a sensible minimum for production to avoid cold start latency on traffic spikes. Enable auto-pause only on non-production environments. Pair it with RDS Proxy for connection management. Watch the capacity metrics for the first few weeks after launch and adjust your ceiling based on what you see.

Enjoy the cloud.

Osama


#AWS #AuroraServerless #AmazonAurora #DatabaseEngineering #ServerlessArchitecture #CloudArchitecture #PostgreSQL #Terraform #InfrastructureAsCode #CloudNative #AmazonWebServices #SolutionsArchitect #CloudComputing #BackendEngineering #DatabasePerformance #CloudCost #TechBlog #RDS #CloudInfrastructure #DevOps

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.