# PostgreSQL connection pools for MENA SaaS: a capacity guide

An autoscaling API can run out of database connections while most of its application instances look healthy. Adding another instance may make the incident worse: it brings another connection pool to the same database.

For a SaaS team running separate Saudi and UAE deployments, the useful unit of planning is each database and every workload that can reach it. A regional architecture diagram does not show the connection demand from rolling deployments, report workers or a forgotten maintenance service.

This guide explains how to budget those connections, choose a pooling mode and test the application assumptions behind it. It compares PgBouncer with selected Amazon RDS Proxy behaviours, not their prices or regional availability. The recommendations are an engineering review framework, not measured performance results.

*Cover: original SultanByte artwork showing application connections passing through a pooler to a bounded PostgreSQL connection budget.*

## Count the fleet, not one process

The [node-postgres pool-sizing guide](https://node-postgres.com/guides/pool-sizing) explicitly asks developers to consider pool limits across all service instances. A limit that seems small in one process can become large when the platform adds replicas.

Create an inventory for each database target. Include web processes, queue workers, scheduled tasks, migrations and operational tools. Record the maximum simultaneous instance count and the maximum connections each instance can open. Include overlap between old and new deployments, rather than assuming the old instances disappear before replacements start.

For a direct-to-database design, multiply each workload's peak instance count by its per-instance connection limit, then add the workloads. That is a configured upper bound, not a prediction that every connection will be open. Compare it with the capacity deliberately allocated to applications.

[PostgreSQL 18 documents](https://www.postgresql.org/docs/18/runtime-config-connection.html) `max_connections`, reserved connections and a final superuser reserve separately. It also warns that raising `max_connections` increases resource allocation, including shared memory. Increasing the limit is therefore a capacity change to test, not a free response to an incident.

Keep an explicit operational allowance. A database that admits all normal application demand but leaves no usable administrative path is difficult to recover. On a managed service, verify the roles and operational access actually available instead of assuming that your administrator is a PostgreSQL superuser.

Maintain separate budgets for the Saudi and UAE deployments. Do not add their spare capacity together unless the approved architecture actually permits workloads to move between them. This guide makes no claim that a particular hosting arrangement satisfies either country's legal requirements.

## A proxy creates another budget

With a pooler, count application-to-pooler connections separately from pooler-to-database connections. The first number describes clients; the second consumes database connection slots. A large client allowance does not mean the database can execute all those requests at once.

[PgBouncer's configuration reference](https://www.pgbouncer.org/config.html) defines `default_pool_size` per user/database pair, while `max_client_conn` limits client connections. It also provides database and user connection limits. These are different controls. Inventory the actual pools and every pooler replica; do not treat a per-process configuration value as a fleet-wide cap.

For a proposed deployment, keep a short capacity record with these fields:

- Database target and approved deployment.
- Application client limit, including deployment overlap.
- Pooler instances, database/user pools and backend limits.
- Direct connections that bypass the pooler.
- Operational reserve, queue policy and owner.

The record should expose exceptions. A reporting worker using a direct endpoint still consumes database slots even if the web API's proxy dashboard looks quiet. Likewise, a separate migration route needs a budget and access controls rather than being an undocumented escape hatch.

For RDS Proxy, [AWS defines `MaxConnectionsPercent`](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/rds-proxy-connections.html) relative to the database's `max_connections`. AWS also reserves connections for proxy operations, so its `MaxDatabaseConnectionsAllowed` metric can be lower than the simple percentage calculation. Use the observed allowance when reviewing headroom.

## Choose the mode from the workload

[PgBouncer distinguishes three modes](https://www.pgbouncer.org/features.html). Session pooling retains a backend connection for the client session. Transaction pooling releases it after the transaction. Statement pooling disallows transactions spanning multiple statements.

For short request transactions, transaction pooling is a candidate worth testing. It is not a transparent replacement for a database connection with persistent session state. Session pooling is a better compatibility candidate when a workload genuinely needs that state, but long-lived clients then retain backend connections for longer.

Make a feature inventory before changing the endpoint. Look for `LISTEN`, temporary objects, session-level advisory locks and code that assumes a setting survives into the next transaction. PgBouncer's feature matrix marks several of those assumptions incompatible with transaction pooling. A successful connection test does not exercise them.

Give exceptional workloads a deliberate route. A notification listener may need a session-capable connection while ordinary HTTP requests use transaction pooling. That exception should have a bounded connection count and a named owner. Do not silently send the whole application through a direct endpoint because one background process needs different semantics.

![Three connection-budget layers: bound application client demand, count pooler backend limits across replicas, and preserve database capacity for other workloads and operations. A separate compatibility check asks when a backend can be reused.](https://cdn.hashnode.com/uploads/covers/60ecf4a0fc37a15ec15655e8/8c9bb37c-d195-43a8-a01b-a7997d90204a.png align="center")

*Original infographic: SultanByte. Proposed capacity-review framework informed by PostgreSQL, PgBouncer, node-postgres and AWS documentation, November 2026. It shows connection boundaries, not measured throughput or a recommended pool size.*

## Keep a transaction on one checked-out client

An application pool and a transaction pooler solve different problems. The application still needs to send every statement in a transaction through the same checked-out client.

The [node-postgres transaction documentation](https://node-postgres.com/features/transactions) expressly warns against using separate `pool.query()` calls for a transaction. Acquire a client, perform the transaction through that client, and release it after completion. Review the error path as carefully as the successful commit: an unreturned client can exhaust the application pool even when the database has capacity.

Keep external network calls and user interaction outside the database transaction where the business operation allows it. Waiting for a remote service while holding a transaction open consumes the backend allocation that transaction pooling is meant to share. If consistency requires coordinating an external effect, design that coordination explicitly rather than holding a database connection indefinitely.

Tenant context deserves a regression test whenever the pool mode changes. SultanByte's [tenant-isolation review](https://www.sultanbyte.com/tenant-isolation-for-mena-saas-a-production-review) covers transaction-scoped context and constrained roles. Here, the release question is whether the same isolation tests still pass through the actual pooler, driver and database versions being deployed.

## Check prepared statements precisely

Avoid the blanket instruction to disable prepared statements whenever pooling is enabled.

PgBouncer's feature documentation distinguishes protocol-level prepared plans from SQL `PREPARE` and `DEALLOCATE`. Its protocol-level support in transaction pooling requires a nonzero `max_prepared_statements`; the SQL-command form remains a different compatibility case. Inspect what the driver and ORM actually send, then test that path against the installed configuration.

Include application upgrades and schema changes in the test. An ORM changing its preparation behaviour can invalidate a previously reasonable endpoint choice. Keep the driver version, pooler version and relevant settings in the deployment record rather than relying on a remembered rule about “prepared statements”.

## RDS Proxy can preserve state by pinning

RDS Proxy's behaviour should not be inferred from PgBouncer's mode names. [AWS describes session pinning](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/rds-proxy-pinning.html): a client becomes attached to one backend connection, preventing other clients from reusing that connection until the session ends.

AWS lists PostgreSQL pinning conditions that include session-state operations and particular SQL commands. Review the PostgreSQL section specifically; the page also documents other database engines with different rules. Do not remove a correctness-critical setting simply to improve a pooling metric.

Measure `DatabaseConnectionsCurrentlySessionPinned` alongside actual workload behaviour. A proxy can be functioning correctly while achieving little sharing because the clients require persistent state. That result changes the capacity calculation, not necessarily the correctness verdict.

AWS's connection guidance also warns that a pinned client sitting idle inside an application pool continues holding its database connection. The two pooling layers can therefore work against the intended efficiency. Reproduce the application's idle periods as well as its busy periods before deciding whether the configuration helps.

## Bound waiting before adding capacity

Treat queue time as part of the request deadline. Distinguish waiting for an application client, waiting for a proxy backend and executing a database query. A query can be fast once admitted while the user waits too long for admission.

AWS documents `ConnectionBorrowTimeout` separately from query timeout. Apply that distinction to the complete design: a browser giving up does not establish that queued or executing database work has stopped. Define cancellation, rollback and response behaviour at each layer, and verify the driver and proxy behaviour in a controlled environment.

For this review framework, cap outstanding work and reject excess demand predictably instead of allowing an unlimited queue. Avoid immediate retries that add more demand to the same exhausted resource. Where a write's outcome is uncertain, recover its business result before issuing a new write.

## Release against a small failure exercise

Run the following proposed acceptance cases with synthetic data:

1. Scale to the declared peak, including a rolling deployment. Require connection use to remain within the allocated budget.
2. Hold one transaction open. Observe where other requests wait and whether their deadlines behave as documented.
3. Exercise every session-dependent feature and prepared-statement path. Require correct results, not just fewer connections.
4. Interrupt a connection during a write. Verify cleanup and outcome recovery without assuming a timeout means rollback.
5. Alternate authorised requests from different tenants over reused connections. Require no context leakage.
6. Saturate the application allowance. Confirm that the intended operational access still works.

Record queue latency, backend use and transaction duration with the test conditions. These are acceptance checks, not a promise of a particular throughput gain.

Approve the pool configuration when the team can explain who owns each connection, when another request may reuse it and what happens when no connection is available. Repeat that review for each regional deployment and every workload exception. Autoscaling the API should not silently rewrite the database's capacity plan.
