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 (ConnectionRequestsBorrowed stalling) means the backend pool is exhausted, not that the database is slow.
RDS Proxy multiplexing and pinning model Multiple client sessions share a small set of backend connections through the proxy. A clean session is multiplexed and returned to the shared pool between transactions, while a session that sets session state is pinned to a dedicated backend connection. Client Session A clean / no state Client Session B clean / no state Client Session C SET / temp table RDS Proxy multiplexer + IAM Shared pool borrow / return Pinned 1:1 dedicated backend Backend conn 1 reused A then B Backend conn 2 locked to C Database max_connections × MaxConnectionsPercent = backend ceiling
Clean sessions multiplex over a shared backend connection; a stateful session is pinned 1:1 and removed from the multiplexing pool.

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