AWS Aurora Serverless v2: Setup, Auto-Pause, RDS Proxy, and Connection Pooling
Set up AWS Aurora Serverless v2 for production. Covers Terraform configuration, ACU scaling, auto-pause for dev environments, RDS Proxy for connection pooling, IAM authentication, and cost comparison with standard Aurora.
Aurora Serverless v2 solves one of the hardest database capacity problems: unpredictable traffic. Standard RDS requires you to provision for peak — you pick an instance size and pay for it whether traffic is 1 req/s or 1,000 req/s. Aurora Serverless v2 scales in fractions of an ACU (Aurora Capacity Unit) within seconds, so you pay for what you actually use.
The tradeoff: it's more expensive per unit than standard Aurora at constant load, but significantly cheaper when traffic is variable or bursty — which describes most SaaS applications.
When Aurora Serverless v2 Makes Sense
| Scenario | Recommendation |
|---|---|
| Steady high traffic (>50% utilization) | Standard Aurora provisioned |
| Variable traffic (SaaS, APIs) | Aurora Serverless v2 ✅ |
| Dev/staging environments | Aurora Serverless v2 with auto-pause ✅ |
| Latency-critical (<5ms DB response) | Standard Aurora (no cold start) |
| Multiple microservices sharing DB | Aurora Serverless v2 + RDS Proxy ✅ |
Terraform Setup
# terraform/aurora-serverless.tf
# DB Subnet Group (subnets must span multiple AZs)
resource "aws_db_subnet_group" "aurora" {
name = "${var.app_name}-aurora"
subnet_ids = var.private_subnet_ids
tags = var.common_tags
}
# Security Group: allow inbound from app tier
resource "aws_security_group" "aurora" {
name = "${var.app_name}-aurora"
vpc_id = var.vpc_id
ingress {
from_port = 5432
to_port = 5432
protocol = "tcp"
security_groups = [aws_security_group.app.id, aws_security_group.rds_proxy.id]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
# Aurora Serverless v2 Cluster
resource "aws_rds_cluster" "app" {
cluster_identifier = var.app_name
engine = "aurora-postgresql"
engine_mode = "provisioned" # Required for Serverless v2
engine_version = "16.4"
database_name = var.db_name
master_username = "postgres"
manage_master_user_password = true # Secrets Manager manages password rotation
db_subnet_group_name = aws_db_subnet_group.aurora.name
vpc_security_group_ids = [aws_security_group.aurora.id]
storage_encrypted = true
kms_key_id = aws_kms_key.aurora.arn
backup_retention_period = 14 # 14 days of automated backups
preferred_backup_window = "03:00-04:00" # UTC
deletion_protection = var.environment == "production"
# Enable Data API (for Lambda without VPC, or AWS Console queries)
enable_http_endpoint = var.enable_data_api
serverlessv2_scaling_configuration {
min_capacity = var.aurora_min_acu # 0.5 ACU minimum for dev, 2 for prod
max_capacity = var.aurora_max_acu # 8 for staging, 64 for production
# Auto-pause: set seconds_until_auto_pause (dev only — pauses after N seconds idle)
# NOT recommended for production (resume takes 15-30 seconds)
seconds_until_auto_pause = var.environment != "production" ? 300 : null
}
skip_final_snapshot = var.environment != "production"
final_snapshot_identifier = var.environment == "production"
? "${var.app_name}-final-snapshot" : null
tags = var.common_tags
}
# Writer instance (required even for Serverless v2)
resource "aws_rds_cluster_instance" "writer" {
identifier = "${var.app_name}-writer"
cluster_identifier = aws_rds_cluster.app.id
instance_class = "db.serverless" # Serverless v2 class
engine = aws_rds_cluster.app.engine
engine_version = aws_rds_cluster.app.engine_version
db_subnet_group_name = aws_db_subnet_group.aurora.name
performance_insights_enabled = true
performance_insights_retention_period = 7 # days (7 free, 731 costs extra)
monitoring_interval = 60 # seconds (Enhanced Monitoring)
monitoring_role_arn = aws_iam_role.rds_monitoring.arn
tags = var.common_tags
}
# Reader instance (Multi-AZ read scaling)
resource "aws_rds_cluster_instance" "reader" {
count = var.environment == "production" ? 1 : 0
identifier = "${var.app_name}-reader"
cluster_identifier = aws_rds_cluster.app.id
instance_class = "db.serverless"
engine = aws_rds_cluster.app.engine
engine_version = aws_rds_cluster.app.engine_version
db_subnet_group_name = aws_db_subnet_group.aurora.name
performance_insights_enabled = true
tags = var.common_tags
}
# Parameter group: PostgreSQL tuning for Aurora
resource "aws_rds_cluster_parameter_group" "app" {
name = "${var.app_name}-pg16"
family = "aurora-postgresql16"
parameter {
name = "shared_preload_libraries"
value = "pg_stat_statements,auto_explain"
}
parameter {
name = "log_min_duration_statement"
value = "1000" # Log queries >1s
}
parameter {
name = "auto_explain.log_min_duration"
value = "5000" # Explain queries >5s
}
parameter {
name = "log_connections"
value = "1"
}
}
output "cluster_endpoint" { value = aws_rds_cluster.app.endpoint }
output "cluster_reader_endpoint" { value = aws_rds_cluster.app.reader_endpoint }
output "cluster_port" { value = aws_rds_cluster.app.port }
☁️ Is Your Cloud Costing Too Much?
Most teams overspend 30–40% on cloud — wrong instance types, no reserved pricing, bloated storage. We audit, right-size, and automate your infrastructure.
- AWS, GCP, Azure certified engineers
- Infrastructure as Code (Terraform, CDK)
- Docker, Kubernetes, GitHub Actions CI/CD
- Typical audit recovers $500–$3,000/month in savings
RDS Proxy for Connection Pooling
Aurora Serverless v2 handles connection pooling less aggressively than standard Aurora. RDS Proxy sits between your app and Aurora, pooling connections and preventing connection storms from Lambda cold starts:
# terraform/rds-proxy.tf
resource "aws_security_group" "rds_proxy" {
name = "${var.app_name}-rds-proxy"
vpc_id = var.vpc_id
egress {
from_port = 5432
to_port = 5432
protocol = "tcp"
security_groups = [aws_security_group.aurora.id]
}
ingress {
from_port = 5432
to_port = 5432
protocol = "tcp"
security_groups = [aws_security_group.app.id]
}
}
# IAM role for Proxy to read Secrets Manager
resource "aws_iam_role" "rds_proxy" {
name = "${var.app_name}-rds-proxy"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "rds.amazonaws.com" }
Action = "sts:AssumeRole"
}]
})
}
resource "aws_iam_role_policy" "rds_proxy_secrets" {
role = aws_iam_role.rds_proxy.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = ["secretsmanager:GetSecretValue"]
Resource = [aws_rds_cluster.app.master_user_secret[0].secret_arn]
}]
})
}
resource "aws_db_proxy" "app" {
name = var.app_name
debug_logging = false
engine_family = "POSTGRESQL"
idle_client_timeout = 1800 # 30 min idle before proxy closes client conn
require_tls = true
role_arn = aws_iam_role.rds_proxy.arn
vpc_security_group_ids = [aws_security_group.rds_proxy.id]
vpc_subnet_ids = var.private_subnet_ids
auth {
auth_scheme = "SECRETS"
secret_arn = aws_rds_cluster.app.master_user_secret[0].secret_arn
iam_auth = "DISABLED" # Set to "REQUIRED" for IAM-only auth
}
}
resource "aws_db_proxy_default_target_group" "app" {
db_proxy_name = aws_db_proxy.app.name
connection_pool_config {
connection_borrow_timeout = 120 # Max seconds to wait for pooled connection
max_connections_percent = 90 # Use 90% of Aurora's max_connections
max_idle_connections_percent = 50 # Keep 50% of connections open when idle
}
}
resource "aws_db_proxy_target" "app" {
db_cluster_identifier = aws_rds_cluster.app.id
db_proxy_name = aws_db_proxy.app.name
target_group_name = aws_db_proxy_default_target_group.app.name
}
output "proxy_endpoint" { value = aws_db_proxy.app.endpoint }
Application Connection
// lib/database/connection.ts
import { Pool } from "pg";
// Always connect to RDS Proxy endpoint (not cluster directly)
// RDS Proxy handles connection pooling and failover
const pool = new Pool({
host: process.env.RDS_PROXY_ENDPOINT!,
port: 5432,
database: process.env.DB_NAME!,
user: process.env.DB_USER!,
password: process.env.DB_PASSWORD!,
// Keep app-level pool small — RDS Proxy handles the real pooling
max: 5,
idleTimeoutMillis: 30_000,
connectionTimeoutMillis: 5_000,
ssl: { rejectUnauthorized: true }, // RDS Proxy enforces TLS
});
pool.on("error", (err) => {
console.error("Unexpected error on idle client", err);
});
export { pool };
// For Prisma, use connection pooling via connection string
// DATABASE_URL=postgresql://user:pass@proxy-endpoint:5432/db?connection_limit=5&pool_timeout=30
// prisma/schema.prisma
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
⚙️ DevOps Done Right — Zero Downtime, Full Automation
Ship faster without breaking things. We build CI/CD pipelines, monitoring stacks, and auto-scaling infrastructure that your team can actually maintain.
- Staging + production environments with feature flags
- Automated security scanning in the pipeline
- Uptime monitoring + alerting + runbook automation
- On-call support handover docs included
Cost Analysis: Aurora Serverless v2 vs Standard Aurora
| Metric | Standard db.r6g.large | Aurora Serverless v2 (2–8 ACU) |
|---|---|---|
| Baseline cost (always-on) | $0.26/hour = $189/mo | 2 ACU × $0.12/ACU-hr = $0.24/hr = $173/mo |
| Peak traffic (8 ACU) | Same $189/mo | 8 ACU × $0.12 = $0.96/hr = variable |
| 50% utilization (avg 4 ACU) | $189/mo | ~$87/mo |
| Dev environment (auto-pause) | $189/mo | ~$5–20/mo |
| RDS Proxy (production) | +$0.015/VPC hour = +$11/mo | +$11/mo |
| Storage | $0.10/GB/month | $0.10/GB/month |
Key insight: At consistent high load, provisioned wins. At variable load (typical SaaS), Serverless v2 saves 40–60%.
ACU Sizing Guide
1 ACU ≈ 2 GB RAM
Recommended starting points:
Development: min=0.5 max=4
Staging: min=1 max=8
Production: min=2 max=32 (adjust based on observed usage)
High-traffic: min=4 max=64
Monitor ServerlessDatabaseCapacity CloudWatch metric to tune min/max.
CloudWatch Alarms
# Alert when hitting max ACU (scale-up ceiling)
resource "aws_cloudwatch_metric_alarm" "aurora_max_capacity" {
alarm_name = "${var.app_name}-aurora-at-max-capacity"
comparison_operator = "GreaterThanOrEqualToThreshold"
evaluation_periods = 3
metric_name = "ServerlessDatabaseCapacity"
namespace = "AWS/RDS"
period = 60
statistic = "Maximum"
threshold = var.aurora_max_acu * 0.95 # Alert at 95% of max
dimensions = { DBClusterIdentifier = aws_rds_cluster.app.id }
alarm_description = "Aurora Serverless approaching max capacity — consider increasing max_capacity"
alarm_actions = [aws_sns_topic.alerts.arn]
}
See Also
- AWS RDS Aurora Production Setup
- AWS RDS Proxy Connection Pooling
- PostgreSQL Connection Pooling with PgBouncer
- Terraform State Management
- AWS Parameter Store vs Secrets Manager
Working With Viprasol
Aurora Serverless v2 cuts database costs for SaaS products with variable traffic — but the Terraform setup, RDS Proxy configuration, and ACU sizing decisions all have non-obvious tradeoffs. Our team has Aurora Serverless v2 in production for multiple SaaS clients, with RDS Proxy for connection pooling, Performance Insights for query optimization, and CloudWatch alarms for capacity monitoring.
What we deliver:
- Terraform: Aurora Serverless v2 cluster with writer + optional reader
- RDS Proxy setup with Secrets Manager integration and connection pool tuning
- Parameter group with pg_stat_statements, auto_explain, and slow query logging
- CloudWatch alarms for capacity ceiling and connection errors
- Application connection configuration for Prisma and pg Pool
Talk to our team about your Aurora Serverless setup →
Or explore our cloud infrastructure services.
About the Author
Viprasol Tech Team
Custom Software Development Specialists
The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.
Need DevOps & Cloud Expertise?
Scale your infrastructure with confidence. AWS, GCP, Azure certified team.
Free consultation • No commitment • Response within 24 hours
Making sense of your data at scale?
Viprasol builds end-to-end big data analytics solutions — ETL pipelines, data warehouses on Snowflake or BigQuery, and self-service BI dashboards. One reliable source of truth for your entire organisation.