AWS RDS Proxy Connection Pooling
This guide is part of Cloud Database Connection Management. It dissects how Amazon RDS Proxy maintains a warm pool of backend database connections, multiplexes thousands of client sessions over that pool, and where the multiplexing model silently breaks down into session pinning. RDS Proxy is a fully managed connection multiplexer that sits between your application fleet and an RDS or Aurora instance for PostgreSQL and MySQL engines. It absorbs connection churn from short-lived clients — Lambda functions, autoscaling containers, serverless workers — that would otherwise overwhelm the database with raw max_connections pressure.
The proxy does not replace a client-side pool; it complements one. A HikariCP, node-postgres, or database/sql pool still governs how many connections each application instance opens to the proxy, while the proxy governs how many backend connections it opens to the database. Misunderstanding that two-tier relationship is the root of most RDS Proxy incidents: undersized MaxConnectionsPercent, pinned sessions that defeat multiplexing, and borrow timeouts that masquerade as database slowness.
Key operational takeaways:
- The proxy multiplexes many client sessions over a smaller set of backend connections; multiplexing only works between transactions, not during them.
- Session pinning collapses a multiplexed session into a dedicated 1:1 backend connection, eroding the pool’s effective capacity.
- Backend pool ceiling is
instance max_connections × MaxConnectionsPercent ÷ 100; size the client pool below that ceiling, not equal to it. - IAM authentication and TLS are enforced at the proxy edge, decoupling credential rotation from the application.
- A borrow timeout (
ConnectionRequestsBorrowedstalling) means the backend pool is exhausted, not that the database is slow.
Foundational mechanics
RDS Proxy maintains a warm set of established backend connections to the target database. When a client opens a session to the proxy, the proxy does not immediately allocate a dedicated backend connection. Instead it assigns one from the shared pool for the duration of a single transaction, then returns it. This is transaction-level multiplexing — conceptually identical to PgBouncer’s transaction mode and described in depth in PgBouncer Transaction vs Statement Pooling. Between transactions, a backend connection is free to serve a different client session.
Connection borrowing. A borrow occurs each time a client begins a unit of work. The proxy borrows an idle backend connection, executes the statements, and on COMMIT or ROLLBACK (or completion of an implicit single-statement transaction) returns it to the pool. Because most client connections are idle most of the time, a pool of 100 backend connections can comfortably serve thousands of client sessions. This is the entire economic argument for the proxy.
Session pinning. Multiplexing is only safe when a transaction leaves no residue on the backend connection. The moment a session sets connection-scoped state — SET of a session variable, a session-level advisory lock, a PREPAREd statement outside the protocol’s extended-query fast path, a temporary table, or SET ROLE — the proxy can no longer hand that backend connection to another client, because the next client would inherit the state. The proxy responds by pinning: it locks the backend connection to that one client session until the client disconnects. Pinning is correct behavior, but it converts a shared connection into a dedicated one and shrinks effective pool capacity. Diagnosing and eliminating pinning is the subject of Resolving RDS Proxy Session Pinning.
IAM authentication. RDS Proxy can enforce IAM database authentication at its edge. The application presents a short-lived IAM auth token instead of a static password; the proxy validates the token and uses Secrets Manager credentials for the actual backend login. This decouples credential rotation from the application — secrets rotate in Secrets Manager without redeploying clients — and centralizes TLS termination. Enabling RequireTLS forces every client leg to use TLS, which interacts with driver-side validation behavior covered in Configuring Connection Validation Queries for AWS RDS Proxy.
Target groups. Each proxy has a default target group that points at a registered database target — an RDS instance or an Aurora cluster endpoint. For Aurora, the proxy tracks reader and writer roles and can route accordingly, which dovetails with the scaling behavior in AWS Aurora Connection Scaling. The target group also carries the ConnectionPoolConfiguration block where the sizing knobs live.
Failover behavior. During an RDS or Aurora failover, the proxy keeps client connections open and re-establishes its backend connections to the promoted instance, masking most of the failover window from the application. The client session survives; only in-flight transactions are aborted. This is a meaningful availability gain over direct connections, where every client would see a hard disconnect and have to reconnect through DNS propagation. The proxy detects the topology change and drains the old backend connections rather than handing out dead sockets.
Concurrency model & topology
The defining property of the proxy is that client concurrency and backend concurrency are decoupled. A client session is cheap — it is a socket and a small amount of proxy-side state. A backend connection is expensive — it is a full database process (PostgreSQL forks a backend per connection) with its own memory and lock footprint. The proxy’s job is to keep the expensive resource small while letting the cheap resource grow.
| Layer | Unit | Governed by | Typical count | Cost per unit |
|---|---|---|---|---|
| Application thread/event loop | In-flight query | App concurrency model | Hundreds per instance | Negligible |
| Client pool → proxy | Client session | Client pool max × instances |
Hundreds to low thousands | One proxy socket |
| Proxy → database | Backend connection | MaxConnectionsPercent ceiling |
Tens to low hundreds | One DB backend process |
A serverless or autoscaling fleet maps poorly onto a database’s max_connections because the fleet’s connection count is a function of instance count, not of actual query concurrency. Twenty Lambda environments that each open one connection consume twenty backend slots even when nineteen are idle between invocations. Routed through the proxy, those twenty client sessions multiplex over perhaps three backend connections. The multiplexing ratio — client sessions per backend connection — is the core metric of pool efficiency and is the number you watch in Diagnostics & telemetry below.
The proxy does not change your application’s concurrency model; it changes how that model lands on the database. A thread-per-request Java service with a HikariCP pool of 20 still issues at most 20 concurrent queries per instance. What the proxy changes is that those 20 connections-to-the-proxy collapse into far fewer connections-to-the-database, and that collapse only holds while sessions remain unpinned.
Precision sizing & timeout orchestration
The backend pool is sized as a percentage of the target database’s max_connections, not as an absolute number. This is the single most consequential design decision. The two governing parameters are MaxConnectionsPercent (the ceiling of backend connections the proxy may open) and MaxIdleConnectionsPercent (how many it keeps warm when demand falls).
| Parameter | Safe range | What it controls | Failure action if wrong |
|---|---|---|---|
MaxConnectionsPercent |
75–100 for a dedicated proxy; lower if multiple consumers share the instance | Hard ceiling on backend connections = max_connections × pct ÷ 100 |
Too low → borrow timeouts under load; too high → starves other clients of the same DB |
MaxIdleConnectionsPercent |
5–50 | Warm idle backend connections held between bursts | Too low → cold-start latency on bursts; too high → wasted backend slots |
IdleClientTimeout |
600–1800 s | How long an idle client session is held before the proxy closes it | Too low → app sees unexpected disconnects; too high → leaked client slots |
ConnectionBorrowTimeout |
120 s (default) | Max wait for a backend connection before failing the borrow | Too low → spurious failures on transient spikes; too high → request threads block |
RequireTLS |
true in production |
Forces TLS on the client leg | Disabled → plaintext credentials in transit |
The backend ceiling math is unforgiving. If the RDS instance reports max_connections = 410 and MaxConnectionsPercent = 90, the proxy will open at most 369 backend connections. Every other consumer of that database — replicas of your app that bypass the proxy, admin tools, the RDS monitoring agent — competes for the remaining 41. When MaxConnectionsPercent is set too aggressively on a shared instance, the proxy will happily exhaust the server and trigger FATAL: remaining connection slots are reserved. The detailed exhaustion math and its symptoms are worked through in Diagnosing RDS Proxy Borrow Timeouts.
Critically, the client pool must be sized below the backend ceiling, accounting for fan-out. If 20 application instances each run a HikariCP pool with maximumPoolSize = 20, the proxy can see up to 400 concurrent client sessions — which is fine, because they multiplex — but if every one of those sessions pins, you need 400 backend slots and the ceiling of 369 is breached. Size the client pool for steady-state concurrency, not peak theoretical fan-out.
When the proxy helps and when it hurts
RDS Proxy is not free latency. It adds a network hop and a small amount of borrow overhead to every transaction. Whether that cost pays for itself depends entirely on the workload’s connection pattern.
| Workload pattern | Proxy verdict | Why |
|---|---|---|
| Lambda / serverless with high invocation churn | Strong win | Absorbs connection storms that would exhaust max_connections; failover masking |
| Autoscaling containers with bursty traffic | Win | Decouples instance count from backend connection count |
| Many short transactions, no session state | Win | Multiplexing ratio stays high; few backend connections serve many clients |
| Long-lived monolith, stable connection count | Marginal | A well-tuned client pool already bounds connections; the hop adds latency for little gain |
Heavy session state (temp tables, SET, prepared statements) |
Often a loss | Pinning collapses multiplexing; you pay the hop for near-1:1 passthrough |
| Latency-critical single-digit-ms queries | Use with care | The added borrow + hop latency is a larger fraction of total query time |
The decisive question is the multiplexing ratio you can actually achieve. If your sessions stay clean, the proxy turns thousands of clients into tens of backend connections and the latency cost is dwarfed by the connection-pressure relief. If your sessions pin — covered exhaustively in Resolving RDS Proxy Session Pinning — the proxy degrades to a 1:1 passthrough with extra latency, and you would be better served by a well-sized client pool talking directly to the database. Before adopting the proxy, audit the application for connection-scoped state; the audit determines whether the proxy is an asset or dead weight.
A second hurt-case is double pooling. Placing the proxy behind an already-conservative client pool on a stable fleet adds a tier that bounds nothing the client pool was not already bounding. The proxy earns its keep when client connection count is unpredictable — driven by autoscaling or invocation churn — not when it is already flat.
Production configuration examples
Terraform — proxy with conservative pool config:
resource "aws_db_proxy_default_target_group" "this" {
db_proxy_name = aws_db_proxy.app.name
connection_pool_config {
max_connections_percent = 90
max_idle_connections_percent = 25
connection_borrow_timeout = 120
# Statements that force pinning; listing them here prevents pinning.
session_pinning_filters = ["EXCLUDE_VARIABLE_SETS"]
}
}
max_connections_percent = 90 reserves 10% of the instance for non-proxy consumers. session_pinning_filters with EXCLUDE_VARIABLE_SETS tells the proxy not to pin on SET of session variables — use it only when the application’s SET statements are safe to leak across sessions or are reset each transaction.
HikariCP pointing at the proxy endpoint:
spring:
datasource:
url: jdbc:postgresql://app-proxy.proxy-abc123.us-east-1.rds.amazonaws.com:5432/app
hikari:
maximum-pool-size: 15
minimum-idle: 3
connection-timeout: 4000
max-lifetime: 1500000
idle-timeout: 600000
The client maximum-pool-size is deliberately modest because the proxy, not Hikari, absorbs burst fan-out. Set max-lifetime (1,500,000 ms) below the proxy’s IdleClientTimeout so Hikari rotates connections before the proxy reaps them. The same alignment logic for client pools appears in the HikariCP Configuration Deep Dive.
node-postgres against the proxy:
const { Pool } = require('pg');
const pool = new Pool({
host: 'app-proxy.proxy-abc123.us-east-1.rds.amazonaws.com',
port: 5432,
max: 10, // per-process; the proxy multiplexes across processes
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 5000,
ssl: { rejectUnauthorized: true },
});
Keep max small per worker. With the proxy in front, raw connection count to the database is governed by the proxy ceiling, not by the sum of every worker’s max.
Diagnostics & telemetry
RDS Proxy publishes CloudWatch metrics under the AWS/RDS namespace with a ProxyName dimension. The four metrics that matter operationally:
| Metric | Meaning | Alert condition |
|---|---|---|
DatabaseConnections |
Backend connections currently open | Approaching max_connections × MaxConnectionsPercent |
DatabaseConnectionsCurrentlySessionPinned |
Backend connections locked 1:1 by pinning | > 5–10% of DatabaseConnections sustained |
DatabaseConnectionsBorrowLatency |
Microseconds spent waiting to borrow a backend connection | Rising into the thousands; spikes precede borrow timeouts |
ClientConnections |
Active client sessions to the proxy | Useful for confirming multiplexing ratio vs DatabaseConnections |
Compute the multiplexing ratio as ClientConnections ÷ DatabaseConnections. A healthy transaction-multiplexed workload runs a ratio well above 5:1. A ratio near 1:1 means almost every session is pinned, and the proxy is delivering no pooling benefit. Cross-check the backend with pg_stat_activity (or SHOW PROCESSLIST on MySQL) — connections opened by the proxy use the Secrets Manager login user, so they are easy to attribute. For a Prometheus-based view of the upstream client pool feeding the proxy, the patterns in the observability area complement these CloudWatch metrics.
Integration & proxy compatibility
RDS Proxy occupies the same architectural slot as PgBouncer and pgpool-II but with different operational trade-offs — managed lifecycle and IAM integration versus self-hosted flexibility and statement-level pooling. The decision between them, including when a self-hosted proxy is the better fit for serverless Postgres, is laid out in PgBouncer vs RDS Proxy vs pgpool-II.
Do not stack a transaction-mode PgBouncer behind RDS Proxy; the two multiplexers fight over transaction boundaries and double the pinning surface. Choose one multiplexing tier. With a client-side pool (HikariCP, pg, database/sql) in front of the proxy, keep the client pool’s idle and lifetime timeouts strictly shorter than the proxy’s IdleClientTimeout, so the client always closes first and never reuses a socket the proxy has already reaped. The proxy also changes connection-validation semantics: a driver SELECT 1 validation query is answered by whichever backend connection the proxy borrows, which is exactly the subtlety addressed in Configuring Connection Validation Queries for AWS RDS Proxy.
Common failure patterns & remediation
| Symptom | Root cause | Exact fix | Validation |
|---|---|---|---|
DatabaseConnectionsCurrentlySessionPinned high |
Sessions set SET, temp tables, or session-level prepared statements |
Remove session state or add session_pinning_filters |
Pinned metric drops toward 0 |
| Borrow timeout after ~120 s | Backend pool hit MaxConnectionsPercent ceiling |
Raise MaxConnectionsPercent or reduce client pinning |
DatabaseConnectionsBorrowLatency returns to baseline |
FATAL: remaining connection slots are reserved |
Proxy ceiling + non-proxy clients exceed max_connections |
Lower MaxConnectionsPercent or raise instance max_connections |
DatabaseConnections stays under reserved ceiling |
| Intermittent client disconnects | IdleClientTimeout shorter than client pool idle-timeout |
Set client idle-timeout < IdleClientTimeout |
No unexpected connection closed in app logs |
| No pooling benefit (ratio ~1:1) | Nearly all sessions pinned | Audit pinning causes per the pinning guide | ClientConnections ÷ DatabaseConnections rises above 5:1 |
Related
- Cloud Database Connection Management — the parent overview of managed-database pooling across AWS, GCP, and Azure.
- Resolving RDS Proxy Session Pinning — diagnose and eliminate the session state that defeats multiplexing.
- Diagnosing RDS Proxy Borrow Timeouts — borrow latency, ceiling math, and remediation.
- PgBouncer vs RDS Proxy vs pgpool-II — choosing the right multiplexing tier.
- AWS Aurora Connection Scaling — how the proxy interacts with Aurora reader/writer scaling.