AWS RDS RDS Proxy PostgreSQL MySQL database connection pooling

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.

Jerzy Kopaczewski ·
You deployed RDS Proxy expecting it to solve your connection exhaustion problems. Instead, you are seeing ConnectionBorrowTimeout errors, sessions getting pinned (defeating the purpose of pooling), or latency increasing compared to direct connections. This runbook covers correct configuration, tuning parameters, and the scenarios where RDS Proxy is the wrong tool.

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:

  1. MaxConnectionsPercent is too low
  2. Your workload has too many long-running queries holding connections
  3. 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

CausePostgreSQLMySQLWorkaround
SET statements (session variables)✅ Pins✅ PinsUse InitQuery or avoid per-session SETs
Prepared statements✅ Pins✅ PinsUse extended query protocol or server-side prepared statement name reuse
TEMPORARY tables✅ Pins✅ PinsUse CTEs or permanent tables with session_id column
User-defined variables (MySQL @var)N/A✅ PinsPass values via application logic
LOCK TABLE✅ Pins✅ PinsUse row-level locks (SELECT FOR UPDATE)
Cursors (DECLARE/FETCH)✅ Pins✅ PinsUse LIMIT/OFFSET or keyset pagination
LISTEN/NOTIFY✅ PinsN/AUse SNS/SQS instead for event notification
Advisory locks✅ Pins✅ PinsUse 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

ScenarioWhy RDS Proxy is wrongBetter alternative
Application already uses connection pooling (HikariCP, pgbouncer)Double pooling causes unpredictable behaviourTune existing pool; proxy is redundant
Heavy use of prepared statementsSessions pin immediately, nullifying poolingPgBouncer in transaction mode with server-reset
LISTEN/NOTIFY for real-time eventsSessions pin for the entire notification subscriptionDirect connection for subscribers; SNS/SQS for new architectures
Sub-millisecond query latency requirements1-3ms overhead per hopDirect connection with application-level pooling
Long-running transactions (>30 seconds)Holds pooled connection, starves othersDedicated read replica or separate connection for batch jobs
Multi-tenant with >200 databasesHits proxy limit per accountPgBouncer 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 RDS
  • DatabaseConnectionsCurrentlySessionPinned - pinned (unusable for pooling)
  • ClientConnections - how many clients are connected to the proxy
  • DatabaseConnectionsBorrowLatency - how long clients wait for a pooled connection
  • QueryRequests / 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:

  1. Confirm pinning won’t be an issue (check for prepared statements, SET commands, temp tables)
  2. Set MaxConnectionsPercent to 85% (leave admin headroom)
  3. Set ConnectionBorrowTimeout low for APIs (5-10s) to fail fast
  4. Monitor DatabaseConnectionsCurrentlySessionPinned from day one
  5. Compare cost vs self-managed PgBouncer if you are on large instances
Loading...