Home / AWS

AWSHow-toAdvanced

RDS Proxy in front of RDS and Aurora: setup, target health and connection pinning

Put RDS Proxy in front of an RDS or Aurora database to absorb connection storms, fix unhealthy targets, and find and remove the session pinning that stops multiplexing.

Deepak Bhardwaj7 min read

Applies toAmazon RDS Proxy with RDS for MySQL, MariaDB, PostgreSQL, SQL Server and Aurora MySQL/PostgreSQL; AWS CLI v2

When you need it

Use RDS Proxy when the database runs out of connections before it runs out of CPU:

  • Lambda functions or autoscaled containers each open their own connections, and a traffic spike hits max_connections.
  • Errors such as too many connections (MySQL) or FATAL: remaining connection slots are reserved (PostgreSQL) appear during deploys or failovers.
  • DatabaseConnections stays close to the limit while most sessions are idle.

The proxy keeps a pool of database connections and shares them between client connections at transaction boundaries. That sharing (multiplexing) is where the benefit comes from. Pinning turns it off for a session, so most of this post is about pinning.

Note

RDS Proxy doesn't fix slow queries or a database that is short of CPU. If Performance Insights shows the instance is saturated, fix that first.

1. Prerequisites

  • A Secrets Manager secret for each database user the proxy will log in as, containing username and password.
  • An IAM role that RDS can assume (trust principal rds.amazonaws.com). It needs secretsmanager:GetSecretValue on the secrets, and kms:Decrypt on the secret's KMS key if that key is customer managed.
  • Security groups: the proxy's security group must be allowed into the database's security group on the database port. Clients must be allowed into the proxy's security group.
  • Subnets in at least two Availability Zones in the database's VPC.

2. Create the proxy

aws rds create-db-proxy \
  --db-proxy-name app-proxy \
  --engine-family POSTGRESQL \
  --auth '[{"AuthScheme":"SECRETS","SecretArn":"arn:aws:secretsmanager:eu-west-1:111122223333:secret:app-db-user-AbCdEf","IAMAuth":"DISABLED"}]' \
  --role-arn arn:aws:iam::111122223333:role/rds-proxy-app \
  --vpc-subnet-ids subnet-0aaa subnet-0bbb subnet-0ccc \
  --vpc-security-group-ids sg-0proxy \
  --require-tls \
  --idle-client-timeout 1800

aws rds register-db-proxy-targets --db-proxy-name app-proxy \
  --db-instance-identifiers prod-db          # or --db-cluster-identifiers for Aurora

--engine-family takes MYSQL, POSTGRESQL or SQLSERVER. Set IAMAuth to REQUIRED if clients should connect with IAM tokens instead of passwords:

aws rds generate-db-auth-token --hostname app-proxy.proxy-xxxx.eu-west-1.rds.amazonaws.com \
  --port 5432 --username app_user --region eu-west-1

For Aurora, add a read-only endpoint so reporting traffic goes to the readers:

aws rds create-db-proxy-endpoint --db-proxy-name app-proxy \
  --db-proxy-endpoint-name app-proxy-ro --target-role READ_ONLY \
  --vpc-subnet-ids subnet-0aaa subnet-0bbb subnet-0ccc --vpc-security-group-ids sg-0proxy

Then point the application's connection string at the proxy endpoint instead of the database endpoint. The database user names and the database name stay the same.

3. Check target health

A proxy can report available while its target cannot be reached. Always check the target itself:

aws rds describe-db-proxy-targets --db-proxy-name app-proxy \
  --query "Targets[].{id:RdsResourceId,role:Role,state:TargetHealth.State,reason:TargetHealth.Reason,detail:TargetHealth.Description}" \
  --output table
TargetHealth.Reason Likely cause Fix
UNREACHABLE Proxy can't reach the database port Allow the proxy's security group in the database's security group; check NACLs and routes
CONNECTION_FAILED Network reaches the DB but the connection is refused or TLS fails Check the port, rds.force_ssl or require_secure_transport, and the engine version
AUTH_FAILURE Proxy can't log in with the secret Check the secret's username and password, the role's GetSecretValue and kms:Decrypt permissions, and whether rotation changed the password without updating the secret
PENDING_PROXY_CAPACITY Proxy is still scaling or registering Wait a few minutes, then check again

Most AUTH_FAILURE cases come from the IAM role, not the password. Check CloudTrail for GetSecretValue calls made by the proxy role with an error code.

4. Size the pool

The pool is set as a percentage of the database's max_connections:

aws rds modify-db-proxy-target-group --db-proxy-name app-proxy --target-group-name default \
  --connection-pool-config MaxConnectionsPercent=75,MaxIdleConnectionsPercent=25,ConnectionBorrowTimeout=30
  • MaxConnectionsPercent: leave headroom for admin tools, replication and anything that bypasses the proxy. When several proxies share one database, their percentages together must stay under 100.
  • MaxIdleConnectionsPercent: a higher value keeps warm connections for bursts. A lower value releases them back to the database.
  • ConnectionBorrowTimeout (seconds): how long a client waits for a pooled connection before it gets a timeout error. A burst of these errors means the pool is too small or connections are pinned.

Warning

max_connections on RDS is often derived from instance memory by a formula in the parameter group. If you downsize the instance, the proxy's pool shrinks with it. Re-check the pool after any instance class change.

5. Find and remove pinning

The key metric is DatabaseConnectionsCurrentlySessionPinned (namespace AWS/RDS, dimension ProxyName). If it is close to DatabaseConnections, the proxy is mostly passing connections through one-to-one.

aws cloudwatch get-metric-statistics --namespace AWS/RDS \
  --metric-name DatabaseConnectionsCurrentlySessionPinned \
  --dimensions Name=ProxyName,Value=app-proxy \
  --start-time 2026-10-27T06:00:00Z --end-time 2026-10-27T12:00:00Z \
  --period 300 --statistics Maximum

Also compare ClientConnections with DatabaseConnections, and watch DatabaseConnectionsBorrowLatency.

To see why sessions pin, turn on debug logging for a short time. Debug logs can include SQL text, so treat them as sensitive.

aws rds modify-db-proxy --db-proxy-name app-proxy --debug-logging
# reproduce, then turn it off
aws rds modify-db-proxy --db-proxy-name app-proxy --no-debug-logging

Then query the proxy's log group in Logs Insights:

fields @timestamp, @message
| filter @message like /pinned/
| stats count(*) by @message
| sort count(*) desc
| limit 20

Common pinning causes and fixes

Cause Engine Fix
SET of session variables on every connection MySQL, PostgreSQL Move it to the target group's InitQuery, or set it in the parameter group
Variable sets you know are safe to share MySQL only SessionPinningFilters=EXCLUDE_VARIABLE_SETS (not supported for PostgreSQL)
Temporary tables Both Use regular staging tables, or CTEs and subqueries
Text-protocol PREPARE / EXECUTE, named server-side prepared statements Both, varies by driver For pgJDBC, prepareThreshold=0; check your driver's docs
Advisory or table locks (pg_advisory_lock, GET_LOCK, LOCK TABLES) Both Use transaction-scoped locks (pg_advisory_xact_lock) or route that job directly to the database
LISTEN / cursors WITH HOLD PostgreSQL Route these clients straight to the database endpoint

Setting InitQuery together with the pinning filter:

cat > pool.json <<'EOF'
{
  "MaxConnectionsPercent": 75,
  "MaxIdleConnectionsPercent": 25,
  "ConnectionBorrowTimeout": 30,
  "InitQuery": "SET time_zone = '+00:00'",
  "SessionPinningFilters": ["EXCLUDE_VARIABLE_SETS"]
}
EOF

aws rds modify-db-proxy-target-group --db-proxy-name app-proxy --target-group-name default \
  --connection-pool-config file://pool.json

Caution

EXCLUDE_VARIABLE_SETS lets a session variable set by one client stay on a pooled connection that another client then uses. Use it only when every client sets the same values, or when no query depends on session state. Otherwise you get silent correctness bugs, such as the wrong time zone or SQL mode.

Pinning lasts until the client disconnects. Short-lived sessions limit the damage, but long-lived pools in application servers keep pinned connections indefinitely.

6. Lambda specifics

  • Open the connection outside the handler so warm invocations reuse it.
  • Put the function in the proxy's VPC subnets, and allow the function's security group in the proxy's security group.
  • Use reserved concurrency so the function can't open more client connections than the proxy can serve within ConnectionBorrowTimeout.

Prevent it next time

  • Alarm on DatabaseConnectionsCurrentlySessionPinned as a ratio of DatabaseConnections, and on the database's own DatabaseConnections against max_connections.
  • Review pinning logs before each major release that changes the ORM or driver version.
  • Keep a direct database endpoint for migrations, long-running jobs and LISTEN consumers, and send only request-path traffic through the proxy.
  • Make sure secret rotation updates the same secret ARN the proxy references. If you create a new secret instead, the proxy keeps logging in with the old password and targets go to AUTH_FAILURE.