AWS RDS Proxy: Stop Letting Lambda Obliterate Your Database With 10,000 Connections š„
AWS RDS Proxy: Stop Letting Lambda Obliterate Your Database With 10,000 Connections š„
Honest confession: When I first moved our e-commerce backend to Lambda, I felt unstoppable. Auto-scaling! No servers! Infinite concurrency! Beautiful.
Then Black Friday hit. Lambda scaled to 3,000 concurrent functions. Each one opened a fresh database connection. Our RDS PostgreSQL instance ā which could handle maybe 500 connections ā saw 3,000 requests at once and silently screamed into the void before returning FATAL: sorry, too many clients already.
The database was dead. Checkout was down. The boss was calling. I was not having a good day. š
RDS Proxy quietly fixed all of this. Let me show you how.
The Problem: Lambda + RDS = Connection Pool Carnage š
Regular servers (EC2, containers) maintain a connection pool. They open, say, 20 database connections at startup and reuse them forever. Smart, efficient, calm.
Lambda does not do this.
Each Lambda invocation is its own isolated process. Each one opens its own database connection. When you have 100 concurrent Lambdas, you get 100 connections. When you have 3,000? You get 3,000. When you have 0? Those connections hang around awkwardly for a while before timing out.
Lambda concurrency: 3,000 invocations
ā
RDS PostgreSQL max_connections: 500
ā
Result: 2,500 "sorry, too many clients already" errors
ā
Result: You explaining to your manager why checkout is broken on Black Friday
This isn't a Lambda bug. It's just the fundamental mismatch between stateless serverless functions and stateful database connections.
In production, I've deployed this exact mistake across two different projects before I finally learned my lesson. Third time? RDS Proxy from day one. ā
What RDS Proxy Actually Does ā”
RDS Proxy sits between your Lambda functions and your RDS database. Think of it as a bouncer at a nightclub:
- Without proxy: Every Lambda walks directly into the club (database), immediately occupying a table (connection). When 3,000 people show up at once, the club collapses.
- With proxy: The bouncer manages a queue. 500 tables inside (connection pool). Thousands of people can wait at the door. The bouncer efficiently shuffles people in and out. The club stays happy.
The key magic is connection multiplexing. RDS Proxy maintains a fixed pool of database connections and shares them across thousands of Lambda invocations. Your Lambda thinks it has its own connection. The proxy knows better.
3,000 Lambda invocations
ā
RDS Proxy (connection pool: 100 connections)
ā
RDS PostgreSQL (max_connections: 500)
ā
Everyone lives. Database is happy. You sleep at night.
Setting It Up (It's Easier Than You Think) š ļø
Step 1: Create the Proxy via AWS Console (or Terraform)
# terraform (what I actually use in production)
resource "aws_db_proxy" "main" {
name = "my-app-proxy"
debug_logging = false
engine_family = "POSTGRESQL"
idle_client_timeout = 1800
require_tls = true
role_arn = aws_iam_role.rds_proxy_role.arn
vpc_security_group_ids = [aws_security_group.rds_proxy_sg.id]
vpc_subnet_ids = aws_subnet.private[*].id
auth {
auth_scheme = "SECRETS"
description = "RDS credentials"
iam_auth = "DISABLED"
secret_arn = aws_secretsmanager_secret.rds_credentials.arn
}
}
resource "aws_db_proxy_default_target_group" "main" {
db_proxy_name = aws_db_proxy.main.name
connection_pool_config {
connection_borrow_timeout = 120
max_connections_percent = 90
max_idle_connections_percent = 50
}
}
Step 2: Point Your Lambda at the Proxy Endpoint
// Before: connecting directly to RDS
const pool = new Pool({
host: 'my-db.abc123.us-east-1.rds.amazonaws.com', // ā Direct to RDS
port: 5432,
database: 'myapp',
user: process.env.DB_USER,
password: process.env.DB_PASS,
max: 1, // Lambda can only handle 1 connection anyway
});
// After: connecting through the proxy
const pool = new Pool({
host: 'my-app-proxy.proxy-abc123.us-east-1.rds.amazonaws.com', // ā
Proxy endpoint
port: 5432,
database: 'myapp',
user: process.env.DB_USER,
password: process.env.DB_PASS,
max: 1, // Still 1 per Lambda, but the proxy handles the rest
});
That's... basically it. Your Lambda code barely changes. The proxy does the heavy lifting.
Step 3: Use IAM Authentication (Optional But Smart) š
A serverless pattern that saved us from rotating database passwords manually:
const { Signer } = require('@aws-sdk/rds-signer');
const signer = new Signer({
hostname: process.env.PROXY_HOST,
port: 5432,
region: 'us-east-1',
username: 'lambda_user',
});
exports.handler = async (event) => {
// Generate a short-lived auth token instead of storing passwords
const token = await signer.getAuthToken();
const pool = new Pool({
host: process.env.PROXY_HOST,
port: 5432,
database: 'myapp',
user: 'lambda_user',
password: token, // Rotates automatically
ssl: { rejectUnauthorized: true },
});
// Run your queries
const result = await pool.query('SELECT * FROM orders WHERE id = $1', [event.orderId]);
return result.rows[0];
};
No hardcoded passwords. No secrets in environment variables. IAM handles authentication. Auditors love this. I love this. You will too. šŖ
The Numbers That Changed My Mind š
When architecting on AWS, I learned to measure everything before and after. Here's what RDS Proxy actually delivered on our e-commerce backend:
Before RDS Proxy (Lambda ā RDS direct):
Peak concurrent Lambdas: 200
Database connections consumed: 200
Database errors under load: 12% of requests
RDS CPU at peak: 94%
Connection setup overhead: ~150ms per cold start
After RDS Proxy:
Peak concurrent Lambdas: 1,000+
Database connections consumed: 45 (proxy multiplex magic!)
Database errors under load: 0.1%
RDS CPU at peak: 61%
Connection setup overhead: ~20ms (proxy reuses connections!)
The proxy actually made queries faster for warm Lambdas because it reuses existing connections instead of doing the TCP+TLS handshake dance every time. That's 130ms I gave back to users for free. š
Cost: Is It Worth It? š°
Let's be honest ā RDS Proxy costs money:
RDS Proxy pricing (us-east-1):
- $0.015 per vCPU-hour of the underlying database
- Example: db.t3.medium (2 vCPUs) ā $0.03/hour ā ~$22/month
For a small dev/staging environment? That $22/month might feel unnecessary.
For production handling real traffic? It's dramatically cheaper than:
- Scaling up your RDS instance to handle more connections
- Debugging mysterious connection errors at 2 AM
- Explaining to customers why checkout was down for 20 minutes
My rule of thumb: If you're running more than 50 concurrent Lambdas hitting the same database, enable RDS Proxy. The math always works out in your favor.
Cost-saving tip: RDS Proxy automatically pauses during periods of zero traffic. If your dev database is idle for hours overnight, you're not paying for an idle proxy. Serverless billing at its finest! š
Common Pitfalls I Hit So You Don't Have To šŖ¤
Pitfall #1: Wrong Security Group Setup
Error: "Could not connect to the endpoint"
This almost always means your Lambda's security group isn't allowed to reach the proxy's security group. The proxy doesn't live in the same security group as your RDS!
# Lambda security group needs outbound to proxy SG
resource "aws_security_group_rule" "lambda_to_proxy" {
type = "egress"
from_port = 5432
to_port = 5432
protocol = "tcp"
source_security_group_id = aws_security_group.rds_proxy_sg.id
security_group_id = aws_security_group.lambda_sg.id
}
# Proxy security group needs to accept from Lambda SG
resource "aws_security_group_rule" "proxy_from_lambda" {
type = "ingress"
from_port = 5432
to_port = 5432
protocol = "tcp"
source_security_group_id = aws_security_group.lambda_sg.id
security_group_id = aws_security_group.rds_proxy_sg.id
}
I spent three hours debugging this. You're welcome.
Pitfall #2: Pinning (The Silent Performance Killer)
RDS Proxy's multiplexing magic breaks when your database session has state. Things like:
-- These BREAK connection reuse:
SET LOCAL statement_timeout = '5s'; -- Session variable
BEGIN; -- Open transaction left dangling
PREPARE my_statement AS SELECT ...; -- Prepared statements (in some configs)
When the proxy detects session state, it pins that connection to your Lambda ā no more sharing. You lose all the multiplexing benefit.
A serverless pattern that saved us: Keep database interactions stateless. Avoid long-running transactions. Commit early. Don't use session-level settings in Lambda functions.
Pitfall #3: Forgetting the Proxy Needs Secrets Manager
RDS Proxy requires credentials to be stored in Secrets Manager. It won't accept plaintext passwords. This is actually a blessing in disguise (forced good practice!), but it surprises people:
# Create the secret first
aws secretsmanager create-secret \
--name rds/my-app/credentials \
--secret-string '{"username":"lambda_user","password":"your-password"}'
# Then reference it in the proxy config
# secret_arn = "arn:aws:secretsmanager:us-east-1:123456789:secret:rds/my-app/credentials"
Should You Even Use RDS With Serverless? š¤
Fair question. DynamoDB is "natively serverless" and handles any concurrency without proxy tricks. But in production, I've deployed plenty of workloads that genuinely needed relational databases ā complex joins, ACID transactions, existing schemas that weren't worth migrating.
Use RDS + RDS Proxy when:
- ā You have relational data with real foreign keys and joins
- ā Your team knows SQL and doesn't want to learn DynamoDB access patterns
- ā You're migrating an existing relational app to serverless incrementally
- ā You need ACID transactions across multiple tables
Use DynamoDB instead when:
- ā You're building greenfield serverless from scratch
- ā Your access patterns are simple and well-defined
- ā You want zero connection management concerns
I've run both in production. Neither is wrong. But if you're using RDS with Lambda ā seriously, add the proxy. It's a $22/month insurance policy against 3 AM database meltdowns.
TL;DR ā The Quick Version ā”
The problem: Lambda + RDS = connection pool explosion under load.
The solution: RDS Proxy sits in between, maintains a real connection pool, and shares connections across thousands of Lambda invocations.
The setup: Create proxy in console/Terraform, change your DB host to the proxy endpoint, done.
The result: 80% fewer database connections, faster query response times, zero connection errors under load.
The cost: ~$22/month for a db.t3.medium. Worth every penny.
When I architected our e-commerce serverless backend, this single addition made the system bulletproof under traffic spikes. Your checkout flow will thank you. Your database will thank you. Your on-call rotation will thank you. š
Dealing with Lambda + database pain? Connect with me on LinkedIn ā I've made every possible serverless database mistake so you don't have to!
See it in action? Check out my GitHub for real serverless architecture examples.
Go protect your database. It deserves better than 10,000 simultaneous connections. šŖāļø