RDS Multi-AZ doubles your database costs for high availability. Perfect for production, wasteful for dev/staging. Here's how to disable it with Terraform and save 50%.
Quick audit: Check your non-production RDS databases right now.
How many have Multi-AZ enabled?
If the answer is "all of them," you're literally paying double for databases that don't need high availability.
Single-AZ RDS (db.t3.medium): $60/month
Multi-AZ RDS (db.t3.medium): $120/month
For a dev database that:
- Can tolerate 5-10 minute downtime
- Gets wiped weekly anyway
- Nobody uses on weekends
Annual waste: $720 per database π±
Let me show you how to fix this with one line of Terraform.
πΈ The Multi-AZ Tax
What Multi-AZ gives you:
- Automatic failover to standby instance (~60-120 seconds)
- Synchronous replication to standby
- No data loss during failure
- 99.95% availability SLA
What it costs:
- Exactly 2x the instance price (you pay for standby too)
- Exactly 2x the storage (replicated)
- 2x backup storage (from both instances)
Perfect for: Production databases serving customers
Wasteful for: Dev, staging, QA, demo, test environments
π― When to Keep Multi-AZ
Use Multi-AZ when:
- β Production database
- β Customer-facing application
- β Revenue-generating service
- β SLA requires < 2 min failover
- β Zero tolerance for data loss
Don't use Multi-AZ when:
- β Development environment
- β Staging/QA environment
- β Personal projects
- β Internal tools
- β Test databases
- β 5-10 min downtime is acceptable
π οΈ Terraform Implementation
Disable Multi-AZ (Single Change)
Before:
resource "aws_db_instance" "staging" {
identifier = "app-staging"
engine = "postgres"
instance_class = "db.t3.medium"
allocated_storage = 100
storage_type = "gp3"
multi_az = true # β Costing you double!
backup_retention_period = 7
skip_final_snapshot = true
}
After:
resource "aws_db_instance" "staging" {
identifier = "app-staging"
engine = "postgres"
instance_class = "db.t3.medium"
allocated_storage = 100
storage_type = "gp3"
multi_az = false # β Changed! Saves 50%
backup_retention_period = 7
skip_final_snapshot = true
}
Deploy:
terraform apply
# RDS will modify the instance (takes 5-10 minutes)
# Removes standby instance
# Immediate 50% cost reduction
Smart Multi-AZ by Environment
# variables.tf
variable "environment" {
description = "Environment name"
type = string
}
locals {
# Multi-AZ only for production
is_production = var.environment == "production"
multi_az_config = {
production = true
staging = false
dev = false
qa = false
}
}
resource "aws_db_instance" "app" {
identifier = "app-${var.environment}"
engine = "postgres"
instance_class = local.is_production ? "db.r6g.large" : "db.t3.medium"
allocated_storage = 100
storage_type = "gp3"
# Multi-AZ based on environment
multi_az = local.multi_az_config[var.environment]
# Production gets longer retention
backup_retention_period = local.is_production ? 30 : 7
# Production gets final snapshot
skip_final_snapshot = !local.is_production
# Production uses encrypted storage
storage_encrypted = local.is_production
tags = {
Environment = var.environment
MultiAZ = local.multi_az_config[var.environment]
}
}
Production Module with Environment Logic
# modules/rds-instance/main.tf
variable "name" {
description = "Database identifier"
type = string
}
variable "environment" {
description = "Environment (production, staging, dev)"
type = string
validation {
condition = contains(["production", "staging", "dev", "qa"], var.environment)
error_message = "Environment must be production, staging, dev, or qa."
}
}
variable "engine" {
description = "Database engine"
type = string
default = "postgres"
}
variable "instance_class" {
description = "Instance class"
type = string
}
variable "allocated_storage" {
description = "Storage in GB"
type = number
default = 100
}
locals {
is_production = var.environment == "production"
# Environment-specific settings
config = {
multi_az = local.is_production
backup_retention_period = local.is_production ? 30 : 7
skip_final_snapshot = !local.is_production
storage_encrypted = local.is_production
deletion_protection = local.is_production
}
}
resource "aws_db_instance" "this" {
identifier = "${var.name}-${var.environment}"
engine = var.engine
instance_class = var.instance_class
allocated_storage = var.allocated_storage
storage_type = "gp3"
# Environment-based configuration
multi_az = local.config.multi_az
backup_retention_period = local.config.backup_retention_period
skip_final_snapshot = local.config.skip_final_snapshot
storage_encrypted = local.config.storage_encrypted
deletion_protection = local.config.deletion_protection
# Always use automated backups
backup_window = "03:00-04:00"
maintenance_window = "sun:04:00-sun:05:00"
tags = {
Name = "${var.name}-${var.environment}"
Environment = var.environment
MultiAZ = local.config.multi_az
ManagedBy = "terraform"
}
}
output "endpoint" {
value = aws_db_instance.this.endpoint
}
output "multi_az_enabled" {
value = aws_db_instance.this.multi_az
}
Usage
# Production - Multi-AZ enabled
module "db_production" {
source = "./modules/rds-instance"
name = "myapp"
environment = "production"
instance_class = "db.r6g.large"
allocated_storage = 500
}
# Staging - Single-AZ
module "db_staging" {
source = "./modules/rds-instance"
name = "myapp"
environment = "staging"
instance_class = "db.t3.medium"
allocated_storage = 100
}
# Dev - Single-AZ
module "db_dev" {
source = "./modules/rds-instance"
name = "myapp"
environment = "dev"
instance_class = "db.t3.small"
allocated_storage = 50
}
output "databases" {
value = {
production = {
endpoint = module.db_production.endpoint
multi_az = module.db_production.multi_az_enabled
}
staging = {
endpoint = module.db_staging.endpoint
multi_az = module.db_staging.multi_az_enabled
}
dev = {
endpoint = module.db_dev.endpoint
multi_az = module.db_dev.multi_az_enabled
}
}
}
π Real-World Savings
Typical Startup (3 environments)
Before (all Multi-AZ):
Production: db.r6g.large Multi-AZ = $350/month
Staging: db.t3.large Multi-AZ = $120/month
Dev: db.t3.medium Multi-AZ = $120/month
Total: $590/month
Annual: $7,080
After (Production Multi-AZ only):
Production: db.r6g.large Multi-AZ = $350/month (kept)
Staging: db.t3.large Single-AZ = $60/month (saved 50%)
Dev: db.t3.medium Single-AZ = $60/month (saved 50%)
Total: $470/month
Annual: $5,640
Savings: $120/month = $1,440/year (20% reduction!) π
Scale: 10 Non-Prod Databases
10 Γ db.t3.medium Multi-AZ = $1,200/month
Switch to Single-AZ:
10 Γ db.t3.medium Single-AZ = $600/month
Savings: $600/month = $7,200/year (50% reduction!) π°
π‘ Pro Tips
1. Audit All Your Databases
# List all RDS instances with Multi-AZ status
aws rds describe-db-instances \
--query 'DBInstances[*].[DBInstanceIdentifier,MultiAZ,DBInstanceClass]' \
--output table
# Filter only Multi-AZ non-prod
aws rds describe-db-instances \
--query 'DBInstances[?MultiAZ==`true`].[DBInstanceIdentifier]' \
--output text | grep -E '(dev|staging|qa|test)'
2. Use Snapshots for Recovery
Single-AZ doesn't mean no backups:
resource "aws_db_instance" "dev" {
# ... other config ...
multi_az = false # Single-AZ
# Still have automated backups!
backup_retention_period = 7
backup_window = "03:00-04:00"
# Can restore from snapshot if needed
}
Recovery from snapshot takes 5-10 minutes β acceptable for dev/staging.
3. Monitor Downtime (If You Care)
resource "aws_cloudwatch_metric_alarm" "db_down" {
alarm_name = "${var.name}-database-down"
comparison_operator = "LessThanThreshold"
evaluation_periods = 1
metric_name = "DatabaseConnections"
namespace = "AWS/RDS"
period = 60
statistic = "Average"
threshold = 1
alarm_description = "Database has no connections"
dimensions = {
DBInstanceIdentifier = aws_db_instance.dev.id
}
}
For Single-AZ, you'll see downtime during maintenance windows. Plan accordingly.
4. Test Your Disaster Recovery
# Create manual snapshot
aws rds create-db-snapshot \
--db-instance-identifier app-dev \
--db-snapshot-identifier app-dev-manual-snapshot
# Restore from snapshot (test recovery)
aws rds restore-db-instance-from-db-snapshot \
--db-instance-identifier app-dev-restored \
--db-snapshot-identifier app-dev-manual-snapshot
Proves you can recover even without Multi-AZ.
β οΈ What You Lose (Be Honest)
Without Multi-AZ, you lose:
- β Automatic failover (manual recovery needed)
- β Synchronous replication (use backups instead)
- β 99.95% SLA (becomes 99.5% single-AZ)
- β Zero RPO during failure (some data loss possible)
You gain:
- β 50% cost reduction
- β Still have automated backups
- β Can restore from snapshot in 5-10 min
- β Good enough for non-production
For production: Keep Multi-AZ. Worth every penny.
For everything else: Single-AZ + backups is plenty.
π― Quick Decision Matrix
| Database Type | Multi-AZ? | Reason |
|---|---|---|
| Production customer-facing | β Yes | Downtime = lost revenue |
| Production internal | β οΈ Maybe | Depends on impact |
| Staging | β No | Can tolerate downtime |
| QA/Test | β No | Gets reset often anyway |
| Dev | β No | Developers can wait 10 min |
| Demo | β No | Schedule demos around maintenance |
| Personal project | β No | Definitely not |
π Quick Start
# 1. Audit current Multi-AZ usage
aws rds describe-db-instances \
--query 'DBInstances[?MultiAZ==`true`].[DBInstanceIdentifier,DBInstanceClass,MultiAZ]' \
--output table
# 2. Identify non-production databases
# (anything with dev, staging, qa, test in name)
# 3. Update Terraform
# Change: multi_az = true β multi_az = false
# 4. Apply changes
terraform apply
# 5. Watch your bill drop next month π°
π― Summary
Multi-AZ doubles RDS costs for high availability:
- Production: Worth it
- Non-production: Wasteful
The fix:
- One line in Terraform:
multi_az = false - 50% instant savings on non-prod databases
- Still get automated backups
- Recovery time: 5-10 minutes (acceptable)
Typical savings:
- 3 non-prod databases: $1,440/year
- 10 non-prod databases: $7,200/year
Stop paying for high availability in environments that don't need it. Disable Multi-AZ on non-prod and save 50%. π
Disabled Multi-AZ on non-prod? How much did you save? Share in the comments! π¬
Follow for more AWS cost optimization with Terraform! β‘
Top comments (0)