Sizing RDS Proxy MaxConnectionsPercent
This guide is part of AWS RDS Proxy Connection Pooling. RDS Proxy has exactly two sizing dials, both expressed as percentages of the database’s max_connections, and getting them wrong produces one of two symptoms: borrow timeouts while the database sits idle, or a database whose connection ceiling is consumed by a proxy that is not using most of it.
The percentages are not intuitive, mainly because they are percentages of a number that itself depends on instance class and can change. Working out the absolute values is the first step in any sizing exercise.
What the two settings control
| Setting | Applies to | Meaning |
|---|---|---|
MaxConnectionsPercent |
target group | Ceiling on database connections the proxy may open, as a percentage of max_connections |
MaxIdleConnectionsPercent |
target group | How many of those the proxy keeps open when idle, as a percentage of max_connections |
ConnectionBorrowTimeout |
target group | Seconds a client waits for a proxy connection before failing |
IdleClientTimeout |
proxy | Seconds an idle client connection is kept before the proxy closes it |
MaxConnectionsPercent defaults to 100 for PostgreSQL proxies, which means a single proxy may consume the entire database ceiling. That is convenient when the proxy is the only client and dangerous when it is not — migrations, monitoring agents, replication tooling, and a human with psql all need slots that the proxy will happily take.
MaxIdleConnectionsPercent defaults to 50 and must be less than or equal to MaxConnectionsPercent. It sets the floor the pool shrinks to: raising it keeps more connections warm and avoids establishment cost inside a request, at the cost of holding database connections you are not using.
The absolute numbers matter more than the percentages, so compute them:
db.r6g.large, max_connections = 1000 (from the instance class formula)
MaxConnectionsPercent = 85% -> 850 database connections available to the proxy
MaxIdleConnectionsPercent = 20% -> 200 kept warm when traffic is low
headroom left for everything else -> 150
Deriving the values
Work from the fleet, not from the defaults:
- Find the real
max_connections. Do not trust the documented formula alone; query the running instance, because parameter group overrides are common:SHOW max_connections; - Subtract operational headroom. Ten to fifteen percent covers migrations, monitoring, replication and human access. That determines
MaxConnectionsPercent— 85 is a good default. - Estimate concurrent database work. With multiplexing, the proxy needs enough database connections to serve concurrent transactions, not concurrent clients. A fleet of 500 Lambda containers each doing a 20 ms query at 200 requests per second needs roughly
200 x 0.02 = 4concurrent connections by Little’s Law, not 500. - Set
MaxIdleConnectionsPercentfrom the steady state. Enough to serve normal traffic without opening connections in the request path, and no more. Twenty percent of 1000 is 200, which is generous for the example above. - Verify against multiple proxies. Each proxy has its own percentage of the same ceiling. Two proxies at 85% each is 170% of the database — an arithmetic error the console will not stop you making.
Step 5 is the one that bites in multi-environment or multi-service setups. Percentages are per target group, and the database has no idea how many proxies point at it.
aws rds modify-db-proxy-target-group \
--db-proxy-name orders-proxy \
--target-group-name default \
--connection-pool-config '{
"MaxConnectionsPercent": 85,
"MaxIdleConnectionsPercent": 20,
"ConnectionBorrowTimeout": 5,
"SessionPinningFilters": ["EXCLUDE_VARIABLE_SETS"]
}'
The borrow timeout
ConnectionBorrowTimeout is how long a client waits for the proxy to hand it a database connection when all of them are busy. The default is 120 seconds, which is far too long for a request path — a client blocked for two minutes has long since exceeded any sensible request deadline, and meanwhile it is occupying a worker.
Set it to a value below your application’s own acquisition timeout so the failure is attributable:
application pool timeout 10 s
ConnectionBorrowTimeout 5 s <- proxy gives up first, with a clear proxy-side error
Inverting these means every proxy-side saturation shows up as an application pool timeout, and you lose the ability to tell whether the queue was in your pool or in the proxy.
For serverless callers the calculus differs: a Lambda function billed by duration should not wait at all if capacity is unavailable, so a borrow timeout of one to two seconds paired with a retry is often better than a long wait. That reasoning is developed in Serverless Function Connection Management.
Watching the right metrics
RDS Proxy publishes CloudWatch metrics that map directly onto these settings:
| Metric | Reading |
|---|---|
DatabaseConnections |
Actual connections the proxy holds — compare against your absolute ceiling |
DatabaseConnectionsCurrentlySessionPinned |
Connections stuck to one client; a high number destroys multiplexing |
DatabaseConnectionsBorrowLatency |
Time clients spend waiting; rising means the ceiling is binding |
ClientConnections |
Client-side connections — the number the proxy is absorbing |
DatabaseConnectionRequestsWithTLS |
Confirms TLS is in use where required |
The pair to watch together is DatabaseConnections against DatabaseConnectionsBorrowLatency. Latency rising while connections are well below the ceiling means something other than the ceiling is the constraint — usually pinning, or a slow database. Latency rising at the ceiling means MaxConnectionsPercent is genuinely binding and the database has capacity to give.
DatabaseConnectionsCurrentlySessionPinned deserves its own alert. Pinning means the proxy has bound a database connection to one client for the rest of that client’s session, which is exactly the multiplexing you paid for being switched off. Common causes are session-level SET statements, temporary tables, and prepared statements — the EXCLUDE_VARIABLE_SETS pinning filter handles the first category, and the rest need application changes of the kind described in handling session-level features under transaction pooling.
Common failure patterns and remediation
| Symptom | Underlying cause | Remediation |
|---|---|---|
| Borrow timeouts while the database is idle | Connections pinned to clients, so multiplexing is off | Check the pinned metric; add EXCLUDE_VARIABLE_SETS; remove session state |
FATAL: too many connections from the database |
Multiple proxies each configured at a high percentage | Divide the percentage by the number of proxies pointing at the instance |
| No slot for migrations during peak | MaxConnectionsPercent at its default of 100 |
Lower to 85 and keep the remainder as headroom |
| Latency spikes after quiet periods | Idle floor too low; connections opened in the request path | Raise MaxIdleConnectionsPercent |
| Clients blocked for two minutes | ConnectionBorrowTimeout left at its default |
Lower it below the application’s own acquisition timeout |
| Percentages changed but nothing happened | Modified the proxy rather than the target group | Connection pool settings live on the target group |
| Ceiling shifted after an instance resize | max_connections is derived from instance memory |
Recompute absolute values after any instance class change |
| Serverless callers time out under burst | Long borrow timeout holding billable function time | Short borrow timeout plus retry with backoff |
Interaction with the application pool
RDS Proxy does not remove the need for a client-side pool, and the two must be sized together. The application pool bounds queue depth where your timeouts and circuit breakers live; the proxy bounds database connections. A reasonable shape:
per-instance application pool max : 5–10
instances : 40
client connections to the proxy : 200–400
proxy database connections (85%) : 850 ceiling, typically using far fewer
Client connections to the proxy are cheap — that is the entire point — so the application pool can be sized for its own concurrency rather than rationed against the database ceiling. What it must not be is unbounded: without a client-side maximum, a runaway service opens client connections until it hits IdleClientTimeout behaviour or the proxy’s own limits, and the queue moves somewhere you cannot instrument.
Set the application pool’s acquisition timeout above ConnectionBorrowTimeout so the proxy-side failure surfaces with its own error, and keep the connection age cap below IdleClientTimeout (1800 seconds by default) so the proxy never closes a connection your pool still believes in.
Operational boundary
These percentages cap connections; they do not create database capacity. If borrow latency is high because the database is saturated, raising MaxConnectionsPercent sends more concurrent work at an instance that is already the constraint, and latency gets worse. Confirm the database has headroom first — active backend count and wait events, as described in interpreting pg_stat_activity wait events for pool issues.
Pinning is the other boundary, and it is the one that most often makes RDS Proxy underperform expectations. A workload that pins on every session gets no multiplexing at all, at which point the proxy is a hop and a bill. Measure the pinned-connection metric before concluding the sizing is wrong.
Frequently Asked Questions
What does MaxConnectionsPercent default to?
Where do I change these settings?
modify-db-proxy-target-group with --connection-pool-config is the API call; the console exposes it under the target group’s connection pool configuration.How do I know what 85% actually is?
SHOW max_connections; on the instance. The documented instance-class formula is a default that parameter groups frequently override.Can two proxies point at the same database?
max_connections.Why are borrows slow when connections are well below the ceiling?
DatabaseConnectionsCurrentlySessionPinned; connections bound to a single client cannot be multiplexed, so the effective pool is far smaller than the metric suggests.Do I still need a client-side pool?
What should ConnectionBorrowTimeout be?
Related
- AWS RDS Proxy Connection Pooling — the parent guide on proxy behaviour and pinning.
- Serverless Function Connection Management — the caller pattern the proxy most often serves.
- Handling Session-Level Features Under Transaction Pooling — removing the session state that causes pinning.
- Tuning PostgreSQL max_connections and superuser_reserved_connections — the ceiling these percentages are of.
- Handling Aurora Failover in Connection Pools — what the proxy does for you during a failover.