Provisioned Aurora runs at full power 24/7 even when nobody's using it. Aurora Serverless v2 scales down to 0.5 ACU during quiet hours and saves you 60%+. Here's the Terraform setup.
Open CloudWatch right now and look at your Aurora CPU utilization graph.
See that pattern? Spikes during business hours, flatline at night and weekends. You're paying the same amount for both.
Provisioned Aurora doesn't care that it's 3 AM on a Sunday. You're paying for a db.r6g.xlarge whether it's handling 5,000 queries/sec or 5.
Aurora Serverless v2 fixes this. It scales down to 0.5 ACU when idle and up to 128 ACU when traffic spikes β and you only pay for what you use. β‘
πΈ The Math That Hurts
A typical provisioned Aurora setup:
db.r6g.xlarge (4 vCPU, 32GB RAM)
Provisioned: $0.58/hour Γ 730 hours = $423/month
Running 24/7 regardless of usage
Aurora Serverless v2:
Night/weekends (0.5 ACU Γ 16 hrs/day): ~$0.06/hour
Business hours (4 ACU Γ 8 hrs/day): ~$0.48/hour
Average monthly cost: ~$170/month
That's 60% savings β same database, same queries, same performance when you need it. π°
Scale it across environments:
| Environment | Provisioned/Month | Serverless v2/Month | Annual Savings |
|---|---|---|---|
| Dev | $423 (db.r6g.xlarge 24/7) | ~$45 (mostly idle) | $4,536 |
| Staging | $423 (db.r6g.xlarge 24/7) | ~$85 (sporadic use) | $4,056 |
| Production | $423 (db.r6g.xlarge 24/7) | ~$250 (variable traffic) | $2,076 |
| Total | $1,269/mo | $380/mo | $10,668/yr π€― |
Dev and staging environments are where the savings are insane β because they sit idle 80%+ of the time.
π€ What Is an ACU?
ACU = Aurora Capacity Unit. Each ACU is roughly 2GB of RAM + proportional CPU.
| ACU | ~RAM | ~CPU | Cost/Hour |
|---|---|---|---|
| 0.5 | 1GB | Minimal | ~$0.06 |
| 1 | 2GB | ~0.25 vCPU | ~$0.12 |
| 4 | 8GB | ~1 vCPU | ~$0.48 |
| 8 | 16GB | ~2 vCPU | ~$0.96 |
| 16 | 32GB | ~4 vCPU | ~$1.92 |
| 128 | 256GB | ~32 vCPU | ~$15.36 |
You set a min and max ACU. Aurora handles everything in between automatically. No restarts, no downtime, no manual scaling. π―
ποΈ Terraform Implementation
Option 1: New Serverless v2 Cluster
# modules/aurora-serverless-v2/main.tf
variable "environment" {
type = string
}
variable "database_name" {
type = string
}
variable "master_username" {
type = string
}
variable "master_password" {
type = string
sensitive = true
}
locals {
# Scale based on environment
scaling = {
dev = {
min_capacity = 0.5 # Nearly free when idle
max_capacity = 2 # Cap costs in dev
}
staging = {
min_capacity = 0.5
max_capacity = 4
}
prod = {
min_capacity = 2 # Never go below 2 ACU in prod
max_capacity = 32 # Scale up for traffic spikes
}
}
}
resource "aws_rds_cluster" "serverless" {
cluster_identifier = "${var.environment}-aurora-serverless"
engine = "aurora-postgresql"
engine_mode = "provisioned" # Yes, "provisioned" β Serverless v2 uses this
engine_version = "16.4"
database_name = var.database_name
master_username = var.master_username
master_password = var.master_password
# Serverless v2 scaling configuration π
serverlessv2_scaling_configuration {
min_capacity = local.scaling[var.environment].min_capacity
max_capacity = local.scaling[var.environment].max_capacity
}
# Storage
storage_encrypted = true
# Backup
backup_retention_period = var.environment == "prod" ? 14 : 1
preferred_backup_window = "03:00-05:00"
# Maintenance
preferred_maintenance_window = "sun:05:00-sun:07:00"
# Protection
deletion_protection = var.environment == "prod"
skip_final_snapshot = var.environment != "prod"
final_snapshot_identifier = var.environment == "prod" ? "${var.environment}-final-snapshot" : null
tags = {
Environment = var.environment
CostCenter = "serverless-v2"
ManagedBy = "terraform"
}
}
# The instance must be db.serverless class
resource "aws_rds_cluster_instance" "serverless" {
count = var.environment == "prod" ? 2 : 1 # Multi-AZ for prod
identifier = "${var.environment}-aurora-serverless-${count.index}"
cluster_identifier = aws_rds_cluster.serverless.id
instance_class = "db.serverless" # π This is what makes it Serverless v2
engine = aws_rds_cluster.serverless.engine
engine_version = aws_rds_cluster.serverless.engine_version
tags = {
Environment = var.environment
ManagedBy = "terraform"
}
}
output "cluster_endpoint" {
value = aws_rds_cluster.serverless.endpoint
}
output "reader_endpoint" {
value = aws_rds_cluster.serverless.reader_endpoint
}
β οΈ Note:
engine_mode = "provisioned"is correct. Aurora Serverless v2 uses the provisioned engine mode withserverlessv2_scaling_configuration. This is different from Serverless v1 which usedengine_mode = "serverless".
Option 2: Migrate Existing Provisioned β Serverless v2
Already have a provisioned Aurora cluster? You can add Serverless v2 instances alongside existing ones, then swap:
# Step 1: Add a Serverless v2 instance to your existing cluster
resource "aws_rds_cluster" "existing" {
cluster_identifier = "my-existing-cluster"
# ... existing config ...
# Add this block to enable Serverless v2 scaling
serverlessv2_scaling_configuration {
min_capacity = 0.5
max_capacity = 16
}
}
# Step 2: Add a Serverless v2 instance
resource "aws_rds_cluster_instance" "serverless_new" {
identifier = "my-cluster-serverless-0"
cluster_identifier = aws_rds_cluster.existing.id
instance_class = "db.serverless" # π Serverless v2
engine = aws_rds_cluster.existing.engine
engine_version = aws_rds_cluster.existing.engine_version
}
# Step 3: After verifying, remove the old provisioned instance
# (Do this in a separate apply after confirming Serverless v2 works)
# resource "aws_rds_cluster_instance" "old_provisioned" {
# identifier = "my-cluster-provisioned-0"
# instance_class = "db.r6g.xlarge" # Remove this
# }
Migration is zero-downtime. Aurora handles failover between provisioned and serverless instances automatically. π
Step 3: Monitor Scaling Behavior
# monitoring/aurora-scaling.tf
resource "aws_cloudwatch_metric_alarm" "acu_high" {
alarm_name = "${var.environment}-aurora-acu-high"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 3
metric_name = "ServerlessDatabaseCapacity"
namespace = "AWS/RDS"
period = 300
statistic = "Average"
threshold = local.scaling[var.environment].max_capacity * 0.8
alarm_description = "Aurora hitting 80% of max ACU β consider raising max_capacity"
dimensions = {
DBClusterIdentifier = aws_rds_cluster.serverless.cluster_identifier
}
alarm_actions = [aws_sns_topic.db_alerts.arn]
}
resource "aws_cloudwatch_metric_alarm" "acu_stuck_high" {
alarm_name = "${var.environment}-aurora-acu-stuck-high"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 12 # 1 hour sustained
metric_name = "ServerlessDatabaseCapacity"
namespace = "AWS/RDS"
period = 300
statistic = "Minimum"
threshold = local.scaling[var.environment].max_capacity * 0.5
alarm_description = "Aurora sustained above 50% max ACU β may be cheaper as provisioned"
dimensions = {
DBClusterIdentifier = aws_rds_cluster.serverless.cluster_identifier
}
alarm_actions = [aws_sns_topic.db_alerts.arn]
}
resource "aws_sns_topic" "db_alerts" {
name = "${var.environment}-aurora-scaling-alerts"
}
The second alarm is key β if your database never scales down, Serverless v2 might actually cost more than provisioned. The alert tells you when to switch back. Honest optimization. β
β οΈ When Serverless v2 is NOT Cheaper
Be honest with yourself about your workload:
Serverless v2 wins when:
- β Traffic varies significantly (business hours vs nights/weekends)
- β Dev/staging environments that sit idle most of the day
- β Batch processing with bursty database usage
- β New apps with unpredictable traffic patterns
- β Seasonal workloads (e-commerce holidays, tax season)
Provisioned wins when:
- β Consistent 24/7 high utilization (>70% CPU constantly)
- β You can commit to Reserved Instances (additional 30-60% off provisioned)
- β Workloads that never dip below 8+ ACU
The break-even rule: If your database would consistently use more than ~6 ACU 24/7, provisioned with Reserved Instances is likely cheaper. Below that? Serverless v2 wins. π―
β‘ Quick Check: Should You Switch?
# Check your Aurora CPU utilization over the past week
aws cloudwatch get-metric-statistics \
--namespace AWS/RDS \
--metric-name CPUUtilization \
--dimensions Name=DBClusterIdentifier,Value=YOUR-CLUSTER-ID \
--start-time $(date -u -d '7 days ago' +%Y-%m-%dT%H:%M:%S) \
--end-time $(date -u +%Y-%m-%dT%H:%M:%S) \
--period 3600 \
--statistics Average Minimum Maximum \
--query 'sort_by(Datapoints,&Timestamp)[].{Time:Timestamp,Avg:Average,Min:Minimum,Max:Maximum}' \
--output table
If you see Average CPU under 30% with big swings between min and max β Serverless v2 will save you money. π°
π‘ Pro Tips
- Start with dev/staging β Lowest risk, highest savings percentage. Migrate prod only after you trust the scaling behavior
- Set min_capacity = 0.5 for non-prod β This is the lowest and cheapest setting. Your dev database will cost pennies when idle
- Set min_capacity β₯ 2 for prod β Scaling from 0.5 takes a moment. A minimum of 2 ACU keeps prod responsive
-
Monitor
ACUUtilizationβ This CloudWatch metric shows what percentage of your max_capacity is being used. If it's consistently 90%+, raise max_capacity -
MySQL and PostgreSQL both supported β Works with
aurora-mysqlandaurora-postgresql - Mixed clusters work β You can run provisioned + serverless instances in the same cluster during migration
π TL;DR
| Environment | Savings vs Provisioned | Effort |
|---|---|---|
| Dev (mostly idle) | 80-90% | 15 minutes |
| Staging (sporadic) | 60-80% | 15 minutes |
| Production (variable) | 30-60% | 30 minutes + monitoring |
| Production (constant high load) | β May cost more | Don't switch |
Bottom line: If your Aurora database has a heartbeat pattern β busy during the day, quiet at night β Serverless v2 lets you stop paying for the quiet hours. Your dev database sitting at 0.5 ACU overnight costs roughly $0.06/hour instead of $0.58/hour. That adds up fast. π
Check your CloudWatch CPU graph. If it looks like a heartbeat, you're overpaying for the flatline parts. Serverless v2 fixes that. π
Found this helpful? Follow for more AWS cost optimization with Terraform! π¬
Top comments (0)