DEV Community

Cover image for Your Aurora Database Runs at 10% Capacity Most of the Day - Let It Scale to Match 🎒
Suhas Mallesh
Suhas Mallesh

Posted on • Edited on

Your Aurora Database Runs at 10% Capacity Most of the Day - Let It Scale to Match 🎒

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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

⚠️ Note: engine_mode = "provisioned" is correct. Aurora Serverless v2 uses the provisioned engine mode with serverlessv2_scaling_configuration. This is different from Serverless v1 which used engine_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
# }
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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-mysql and aurora-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)