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
Both percentages are of max_connections, not of each other A bar representing the database max_connections. MaxConnectionsPercent marks the portion the proxy may consume. MaxIdleConnectionsPercent marks the smaller portion it keeps warm. The remainder is headroom for operational connections. Both dials are percentages of the database ceiling database max_connections = 1000 MaxConnectionsPercent 85% — up to 850 connections the proxy may open headroom 150 idle floor 20% = 200 opened on demand between the floor and the ceiling Leaving MaxConnectionsPercent at its default of 100 removes the green band entirely, so a busy proxy can leave no slot for a migration or for an operator during an incident. Reserve headroom deliberately, even when the proxy is the only application client.
Reserve headroom below MaxConnectionsPercent so operational connections always have somewhere to go.

Deriving the values

Work from the fleet, not from the defaults:

  1. 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;
  2. Subtract operational headroom. Ten to fifteen percent covers migrations, monitoring, replication and human access. That determines MaxConnectionsPercent — 85 is a good default.
  3. 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 = 4 concurrent connections by Little’s Law, not 500.
  4. Set MaxIdleConnectionsPercent from 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.
  5. 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.

Borrow latency read against connection count Rising borrow latency splits into two cases. If database connections are at the configured ceiling, the ceiling is binding and can be raised. If they are well below it, the constraint is pinning or a slow database, and raising the ceiling will not help. Borrow latency alone does not say what to change borrow latency rising is DatabaseConnections at the configured ceiling? yes no the ceiling is binding raise MaxConnectionsPercent pinning or a slow database check the pinned-connection metric only one of these two branches is fixed by a configuration change Raising the ceiling on the right-hand branch consumes database capacity without improving latency.
Compare borrow latency against the connection count before changing either percentage.

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.

Two pools in series, each bounding something different The application pool bounds queue depth where timeouts and circuit breakers live. The proxy bounds database connections. The database enforces the final ceiling. Each layer bounds a different thing, and all three are needed application pool queue depth, timeouts, breakers client connections cheap — this is the ratio you bought proxy pool MaxConnectionsPercent database max_connections Removing the application pool moves the queue somewhere you cannot instrument or time out. Removing the proxy limit lets one target group consume the whole database ceiling.
Keep both pools bounded. The application pool owns queue behaviour; the proxy owns the database ceiling.

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?
100 for PostgreSQL proxies, meaning the proxy may consume the entire database ceiling. Lower it to reserve headroom for operational connections.
Where do I change these settings?
On the proxy’s target group, not the proxy itself. 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?
Query 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?
Yes, and each gets its own percentage of the same ceiling. Divide the percentage by the number of proxies, or you will exceed max_connections.
Why are borrows slow when connections are well below the ceiling?
Almost always pinning. Check 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?
Yes. It bounds queue depth at the layer where your timeouts and breakers live, and it avoids a connection setup per request. Keep it small and always bounded.
What should ConnectionBorrowTimeout be?
Below your application’s own acquisition timeout, so proxy-side saturation is attributable. Five seconds suits a request path; one to two seconds suits a serverless caller that should retry instead of waiting.