AWS RDS Proxy: configuration, tuning, and known limitations
Configure AWS RDS Proxy correctly: MaxConnectionsPercent, idle connection management, pinning avoidance, and the scenarios where RDS Proxy hurts more than it helps.
If you haven’t yet hit connection limits and want to understand the root problem, start with our RDS connection limit exceeded runbook. This page assumes you have already deployed or are evaluating RDS Proxy.
For architecture guidance on connection pooling patterns in production, book a consulting session.
When RDS Proxy helps
RDS Proxy is useful in specific scenarios:
- Lambda → RDS - Lambda functions open new connections per invocation. Without pooling, a burst of 500 Lambda executions creates 500 database connections simultaneously. RDS Proxy multiplexes these over a smaller connection pool.
- Many microservices, few queries each - 20 services maintaining connection pools of 10 each = 200 connections. RDS Proxy can serve all of them from 50 actual database connections.
- Failover speed - RDS Proxy detects Multi-AZ failover faster than DNS propagation and redirects traffic within seconds rather than ~30s.
# Check if RDS Proxy would help: count distinct client connections vs active queries
psql -h your-rds.cluster-abc.eu-central-1.rds.amazonaws.com -U master -d mydb -c "
SELECT
count(*) AS total_connections,
count(*) FILTER (WHERE state = 'active') AS actually_running,
count(*) FILTER (WHERE state = 'idle') AS idle_wasting_slots
FROM pg_stat_activity
WHERE backend_type = 'client backend';"
If idle_wasting_slots is >70% of total_connections, RDS Proxy will help significantly.
Core configuration parameters
MaxConnectionsPercent
The maximum percentage of max_connections on the RDS instance that the proxy can use.
# Default: 100 (proxy can use all available connections)
# Recommended: 80-90 (leave headroom for direct admin access)
aws rds modify-db-proxy-target-group \
--db-proxy-name my-proxy \
--target-group-name default \
--connection-pool-config "MaxConnectionsPercent=85"
Why not 100%? If the proxy uses all connections and you need to connect directly for debugging (via psql as superuser), you will be locked out. Leave 10-15% as a safety margin.
MaxIdleConnectionsPercent
The maximum percentage of MaxConnectionsPercent connections that the proxy keeps open while idle.
# Default: 50
# For bursty workloads (Lambda): set higher (70-80)
# For steady workloads: default is fine
aws rds modify-db-proxy-target-group \
--db-proxy-name my-proxy \
--target-group-name default \
--connection-pool-config "MaxIdleConnectionsPercent=70"
Trade-off: Higher values = faster response to bursts (connections are pre-opened). Lower values = fewer idle connections consuming memory on the RDS instance.
ConnectionBorrowTimeout
How long a client waits (in seconds) for a connection from the proxy pool before getting an error.
# Default: 120 seconds
# For APIs with tight SLAs: 5-10 seconds (fail fast, return 503)
# For batch jobs: 120+ is fine
aws rds modify-db-proxy-target-group \
--db-proxy-name my-proxy \
--target-group-name default \
--connection-pool-config "ConnectionBorrowTimeout=10"
Critical: If you see ConnectionBorrowTimeout errors in CloudWatch, it means the pool is exhausted. This is NOT a timeout to increase - it is a signal that either:
MaxConnectionsPercentis too low- Your workload has too many long-running queries holding connections
- Sessions are pinned (see below)
InitQuery (Session Initialization)
SQL executed when a fresh connection is established to the database:
# PostgreSQL: set application_name for debugging
aws rds modify-db-proxy-target-group \
--db-proxy-name my-proxy \
--target-group-name default \
--connection-pool-config "InitQuery=SET application_name='rds-proxy-pool'"
# MySQL: set timezone
# InitQuery="SET time_zone='+00:00'"
Keep InitQuery minimal. Every statement here runs on each new connection creation.
Connection pinning: the silent killer
Pinning means a client session gets locked to a specific database connection and cannot be multiplexed. When a session is pinned, RDS Proxy cannot reuse that connection for other clients - defeating the entire purpose of pooling.
What causes pinning
| Cause | PostgreSQL | MySQL | Workaround |
|---|---|---|---|
| SET statements (session variables) | ✅ Pins | ✅ Pins | Use InitQuery or avoid per-session SETs |
| Prepared statements | ✅ Pins | ✅ Pins | Use extended query protocol or server-side prepared statement name reuse |
| TEMPORARY tables | ✅ Pins | ✅ Pins | Use CTEs or permanent tables with session_id column |
| User-defined variables (MySQL @var) | N/A | ✅ Pins | Pass values via application logic |
| LOCK TABLE | ✅ Pins | ✅ Pins | Use row-level locks (SELECT FOR UPDATE) |
| Cursors (DECLARE/FETCH) | ✅ Pins | ✅ Pins | Use LIMIT/OFFSET or keyset pagination |
| LISTEN/NOTIFY | ✅ Pins | N/A | Use SNS/SQS instead for event notification |
| Advisory locks | ✅ Pins | ✅ Pins | Use DynamoDB for distributed locking |
Detecting pinning
# CloudWatch metric: DatabaseConnectionsCurrentlySessionPinned
aws cloudwatch get-metric-statistics \
--namespace AWS/RDS \
--metric-name DatabaseConnectionsCurrentlySessionPinned \
--dimensions Name=ProxyName,Value=my-proxy \
--start-time $(date -u -v-1H +%Y-%m-%dT%H:%M:%S) \
--end-time $(date -u +%Y-%m-%dT%H:%M:%S) \
--period 300 \
--statistics Average Maximum
# If Maximum is close to your MaxConnectionsPercent * max_connections,
# pinning is consuming your entire pool
Rule of thumb: If >30% of connections are pinned at any time, RDS Proxy is not providing meaningful value for that workload.
Known limitations
1. No support for IAM database authentication from all clients
RDS Proxy supports IAM auth, but the token must be generated for the proxy endpoint, not the RDS instance. Some ORM connection libraries do not handle this correctly.
2. Latency overhead
RDS Proxy adds 1-3ms of latency per query. For high-frequency, low-latency queries (caching layers, real-time scoring), this overhead accumulates:
Direct RDS: query_time + 0.3ms network
Via RDS Proxy: query_time + 1.5ms proxy + 0.3ms network
For 100 queries per request, that is an extra 120-300ms. Not acceptable for latency-sensitive workloads.
3. Single-AZ proxy deployment (per target group)
RDS Proxy deploys across AZs, but if your RDS instance is single-AZ and fails, the proxy still needs to wait for recovery. Proxy does NOT provide HA if the underlying database is single-AZ.
4. No connection pool per database user
The proxy pool is shared across all authenticated users. If one user’s queries are slow and hold connections, they starve all other users. There is no per-user quota or fair scheduling.
5. Max 200 proxies per account per region
Hard limit. For multi-tenant architectures with database-per-tenant, you will hit this quickly.
6. No support for RDS Custom or self-managed databases
RDS Proxy only works with RDS and Aurora. Not RDS Custom, not EC2-hosted PostgreSQL, not self-managed databases.
7. Cost adds up
RDS Proxy charges per vCPU-hour of the associated RDS instance:
Pricing (eu-central-1): $0.015 per vCPU per hour
db.r6g.large (2 vCPU): $0.015 × 2 × 730 = ~$22/month
db.r6g.4xlarge (16 vCPU): $0.015 × 16 × 730 = ~$175/month
For large instances, the proxy cost is significant. Compare against running PgBouncer on a t3.small (~$15/month) with more control.
When NOT to use RDS Proxy
| Scenario | Why RDS Proxy is wrong | Better alternative |
|---|---|---|
| Application already uses connection pooling (HikariCP, pgbouncer) | Double pooling causes unpredictable behaviour | Tune existing pool; proxy is redundant |
| Heavy use of prepared statements | Sessions pin immediately, nullifying pooling | PgBouncer in transaction mode with server-reset |
| LISTEN/NOTIFY for real-time events | Sessions pin for the entire notification subscription | Direct connection for subscribers; SNS/SQS for new architectures |
| Sub-millisecond query latency requirements | 1-3ms overhead per hop | Direct connection with application-level pooling |
| Long-running transactions (>30 seconds) | Holds pooled connection, starves others | Dedicated read replica or separate connection for batch jobs |
| Multi-tenant with >200 databases | Hits proxy limit per account | PgBouncer fleet or application-level routing |
Terraform configuration
resource "aws_db_proxy" "app" {
name = "app-proxy"
debug_logging = false
engine_family = "POSTGRESQL"
idle_client_timeout = 1800
require_tls = true
role_arn = aws_iam_role.proxy.arn
vpc_security_group_ids = [aws_security_group.proxy.id]
vpc_subnet_ids = aws_subnet.private[*].id
auth {
auth_scheme = "SECRETS"
description = "App database credentials"
iam_auth = "DISABLED"
secret_arn = aws_secretsmanager_secret.db_credentials.arn
}
}
resource "aws_db_proxy_default_target_group" "app" {
db_proxy_name = aws_db_proxy.app.name
connection_pool_config {
max_connections_percent = 85
max_idle_connections_percent = 70
connection_borrow_timeout = 10
init_query = "SET application_name='proxy-pool'"
}
}
resource "aws_db_proxy_target" "app" {
db_proxy_name = aws_db_proxy.app.name
target_group_name = aws_db_proxy_default_target_group.app.name
db_instance_identifier = aws_db_instance.primary.identifier
}
Monitoring checklist
Set CloudWatch alarms on these metrics:
# Critical: pool exhaustion
aws cloudwatch put-metric-alarm \
--alarm-name "RDSProxy-PoolExhausted" \
--metric-name DatabaseConnectionsBorrowLatency \
--namespace AWS/RDS \
--dimensions Name=ProxyName,Value=my-proxy \
--threshold 5000 \
--comparison-operator GreaterThanThreshold \
--evaluation-periods 3 \
--period 60 \
--statistic Average \
--alarm-actions arn:aws:sns:eu-central-1:ACCOUNT:ops-alerts
# Warning: pinning ratio climbing
# Use custom metric from Lambda that computes:
# DatabaseConnectionsCurrentlySessionPinned / DatabaseConnections * 100
Key metrics to watch:
DatabaseConnections- total connections proxy holds to RDSDatabaseConnectionsCurrentlySessionPinned- pinned (unusable for pooling)ClientConnections- how many clients are connected to the proxyDatabaseConnectionsBorrowLatency- how long clients wait for a pooled connectionQueryRequests/QueryDatabaseResponseLatency- throughput and latency
Troubleshooting ConnectionBorrowTimeout
If you are getting ConnectionBorrowTimeout errors:
# Step 1: Check if pinning is the cause
# If pinned connections are >50% of total, fix pinning first (see table above)
# Step 2: Check if max_connections on RDS is the bottleneck
psql -h your-rds-instance -U master -c "SHOW max_connections;"
# Compare with: MaxConnectionsPercent / 100 * max_connections = actual proxy pool size
# Step 3: Check for long-running queries holding connections
psql -h your-rds-instance -U master -c "
SELECT pid, now() - pg_stat_activity.query_start AS duration, query, state
FROM pg_stat_activity
WHERE (now() - pg_stat_activity.query_start) > interval '30 seconds'
AND state != 'idle'
ORDER BY duration DESC;"
# Step 4: If none of the above - scale up RDS instance (more max_connections)
# or add read replicas and route read traffic there
Need help with database connection architecture?
Connection pooling strategy depends on your application patterns, transaction behaviour, and scale. A 30-minute consultation can save weeks of trial-and-error with proxy configuration. Book a call or Send us a message.
Summary
RDS Proxy is a good fit for Lambda-to-RDS patterns and applications with many idle connections. It is a poor fit when sessions use features that cause pinning, when latency matters, or when applications already implement connection pooling.
Before deploying RDS Proxy:
- Confirm pinning won’t be an issue (check for prepared statements, SET commands, temp tables)
- Set
MaxConnectionsPercentto 85% (leave admin headroom) - Set
ConnectionBorrowTimeoutlow for APIs (5-10s) to fail fast - Monitor
DatabaseConnectionsCurrentlySessionPinnedfrom day one - Compare cost vs self-managed PgBouncer if you are on large instances