MongoDB Driver Connection Pools
This guide is part of Pool Architecture & Algorithm Fundamentals. MongoDB’s official drivers all implement the same connection pool specification, which makes their behaviour unusually consistent across languages — and unusually easy to misconfigure in one way that no relational pool shares: a MongoClient does not hold one pool, it holds one pool per server it knows about. A client connected to a three-member replica set with the default maxPoolSize of 100 can open 300 connections, and a client pointed at a sharded topology opens a pool per mongos.
The pool also behaves differently during failure. When a server is marked unavailable, the driver clears its pool — closing every connection to that server at once — and the reconnect that follows is throttled by maxConnecting, a setting with no equivalent in most SQL drivers. Understanding those two behaviours explains most MongoDB connection incidents.
Key operational takeaways:
maxPoolSize(default 100) is per server, perMongoClient. The fleet total isclients × servers × maxPoolSize, and that is the number the server’s connection limit must accommodate.minPoolSize(default 0) connections are opened eagerly and kept, so a non-zero minimum multiplies across servers and processes in the same way.maxConnecting(default 2) limits how many connections one pool may be establishing at once. It is what prevents a connection storm after a pool is cleared, at the cost of slower recovery.- Checkout waits are bounded by
waitQueueTimeoutMSin older drivers and by the unifiedtimeoutMSin current ones; when neither is set, a checkout can wait for the fullserverSelectionTimeoutMS. - Pool events —
ConnectionCheckOutFailed,ConnectionPoolCleared,ConnectionCreated— are the diagnostic surface. Every official driver can emit them, and they distinguish saturation from failover from slow connection establishment.
Foundational mechanics
Every driver implements the Connection Monitoring and Pooling specification, so the vocabulary is shared. A pool belongs to one server address. When an operation needs a connection, the driver first performs server selection — choosing a server that satisfies the read preference and is known to be healthy, waiting up to serverSelectionTimeoutMS (30 seconds by default) — and only then checks a connection out of that server’s pool.
Checkout follows a fixed sequence. If an idle connection is available it is returned immediately. If not, and the pool is below maxPoolSize, the driver creates a connection — but only if fewer than maxConnecting connections are already being established; otherwise the request waits for either an idle connection or a free establishment slot. If the pool is at maxPoolSize, the request waits in the queue until a connection is returned or the timeout expires.
This design has an important consequence: the pool never opens a burst of connections. With maxConnecting = 2, a cold pool serving 50 simultaneous operations establishes connections two at a time, and the waiting operations are served as each one becomes ready. It protects the server from thundering herds, and it also means recovery after a disruption is deliberately gradual.
Pool clearing is the behaviour that surprises people. When the driver marks a server as Unknown — after a network error, a failover, or a heartbeat failure — it clears that server’s pool: all connections are closed and in-progress checkouts fail. A generation counter attached to the pool invalidates any connection created before the clear. When the server returns, the pool refills from zero, throttled by maxConnecting. A replica set failover therefore produces a short burst of errors followed by a recovery whose speed is set by the pool’s establishment rate, not by the application’s demand.
Connection lifetime is governed by maxIdleTimeMS (0, meaning no idle limit, by default) and by the server’s own timeouts. Unlike most SQL drivers there is no maximum lifetime setting; connections live until they are idle beyond maxIdleTimeMS, closed by the server, or discarded by a pool clear.
Monitoring connections are separate. Each MongoClient maintains dedicated monitoring connections to each server — typically two per server with modern streaming monitoring — that do not come from maxPoolSize. They are small in number but they do count against the server’s connection limit, which matters when many clients connect to a small cluster.
Operational Boundary: The pool bounds connections per server per client. Server selection, read preference and topology discovery decide which pool an operation uses; they are separate mechanisms with their own timeouts.
Precision sizing & timeout orchestration
| Parameter | Default | Scope | Recommended range | Operational purpose |
|---|---|---|---|---|
maxPoolSize |
100 | per server, per client | from concurrent operations | Hard cap on connections to one server |
minPoolSize |
0 | per server, per client | 0–5 | Warm connections kept open |
maxConnecting |
2 | per pool | 2–5 | Throttles establishment during warm-up and recovery |
maxIdleTimeMS |
0 (no limit) | per pool | 30,000–300,000 | Close idle connections; required behind NAT |
waitQueueTimeoutMS |
none in newer drivers | per pool | 1,000–5,000 | Bound checkout wait (older drivers) |
timeoutMS |
none | per client or operation | request deadline | Unified client-side timeout covering selection, checkout and execution |
serverSelectionTimeoutMS |
30,000 | per client | 3,000–15,000 | How long to look for a suitable server |
connectTimeoutMS |
30,000 | per connection | 3,000–10,000 | TCP and handshake timeout |
socketTimeoutMS |
none | per connection | above the slowest operation | Abort a stalled socket |
Size maxPoolSize from concurrent operations against that server, not from request concurrency:
maxPoolSize = ceil( operations_per_second × mean_operation_seconds ÷ target_utilisation )
A service issuing 4,000 operations per second with a mean server-side time of 1.5 ms has 4000 × 0.0015 = 6 operations in flight; at 70 % utilisation, maxPoolSize = 9. The default of 100 is eleven times larger than needed, which matters because it is also eleven times more connections the server must be able to accept during a reconnect.
Then check the fleet against the deployment’s limit:
fleet_connections = processes × clients_per_process × servers × maxPoolSize + monitoring_connections
Twelve processes, one client each, three members and maxPoolSize = 9 gives 324 pooled connections plus monitoring — comfortable on any tier. The same fleet at the default would be 3,600, which exceeds the connection limit of many managed tiers.
Order the timeouts so the innermost expires first. With the unified timeoutMS, a single value covers server selection, checkout and execution for an operation, which removes most of the ordering problem; where timeoutMS is not available, keep waitQueueTimeoutMS below the request deadline and serverSelectionTimeoutMS short enough that a genuinely unavailable topology fails fast.
Production configuration examples
Node.js. Set the pool on the client, and reuse one client for the whole process:
import { MongoClient } from "mongodb";
export const client = new MongoClient(process.env.MONGODB_URI, {
maxPoolSize: 9,
minPoolSize: 2,
maxConnecting: 2,
maxIdleTimeMS: 60_000,
serverSelectionTimeoutMS: 5_000,
connectTimeoutMS: 5_000,
timeoutMS: 3_000, // unified deadline for selection, checkout and execution
appName: "orders-api",
retryWrites: true,
});
await client.connect(); // warms minPoolSize connections
Python (PyMongo / Motor). The same options, in snake case or as URI parameters:
from pymongo import MongoClient
client = MongoClient(
os.environ["MONGODB_URI"],
maxPoolSize=9,
minPoolSize=2,
maxIdleTimeMS=60_000,
serverSelectionTimeoutMS=5_000,
connectTimeoutMS=5_000,
waitQueueTimeoutMS=2_000,
appname="orders-api",
)
Java. Configure the pool through MongoClientSettings:
MongoClientSettings settings = MongoClientSettings.builder()
.applyConnectionString(new ConnectionString(System.getenv("MONGODB_URI")))
.applicationName("orders-api")
.applyToConnectionPoolSettings(b -> b
.maxSize(9)
.minSize(2)
.maxConnecting(2)
.maxConnectionIdleTime(60, TimeUnit.SECONDS)
.maxWaitTime(2, TimeUnit.SECONDS))
.applyToClusterSettings(b -> b.serverSelectionTimeout(5, TimeUnit.SECONDS))
.build();
MongoClient client = MongoClients.create(settings);
One client per process. A MongoClient is designed to be created once and shared; it is thread-safe and manages its own topology monitoring. Creating a client per request — still common in serverless handlers — produces a new topology scan, new monitoring connections and a new pool each time, which is the fastest way to exhaust a cluster’s connection limit.
Diagnostics & telemetry
The pool emits events that every official driver exposes. Subscribing to them gives precise metrics without polling:
const inUse = new Map();
client.on("connectionCheckOutStarted", () => metrics.checkoutStarted.inc());
client.on("connectionCheckedOut", (ev) => inUse.set(ev.address, (inUse.get(ev.address) ?? 0) + 1));
client.on("connectionCheckedIn", (ev) => inUse.set(ev.address, (inUse.get(ev.address) ?? 1) - 1));
client.on("connectionCheckOutFailed", (ev) => metrics.checkoutFailed.labels(ev.reason).inc());
client.on("connectionPoolCleared", (ev) => metrics.poolCleared.labels(ev.address).inc());
client.on("connectionCreated", (ev) => metrics.connectionCreated.labels(ev.address).inc());
connectionCheckOutFailed carries a reason, which is the single most informative field in MongoDB pool diagnostics: timeout means the pool was saturated or connections could not be established quickly enough, connectionError means establishment itself failed, and poolClosed means the client was closing.
On the server side, serverStatus reports what the deployment sees:
db.adminCommand({ serverStatus: 1 }).connections
// { current: 412, available: 1088, totalCreated: 90233, active: 37, threaded: 412 }
A totalCreated that climbs steeply while current stays flat is churn — connections being closed and reopened, usually by an aggressive maxIdleTimeMS, a load balancer idle timeout, or repeated pool clears.
Saturation versus establishment versus topology. If connectionCheckOutFailed with reason timeout rises while connectionCreated is flat and the pool is at maxPoolSize, the pool is saturated: operations are queueing for busy connections. If connectionCreated is rising and checkouts still time out, establishment is the bottleneck — raise maxConnecting or investigate TLS and authentication latency. If operations fail before checkout with server selection errors, the problem is topology, not the pool.
Integration & proxy compatibility
Atlas and managed tiers impose a connection limit per cluster tier, and that limit counts every connection including monitoring. Sizing maxPoolSize from operations in flight, rather than leaving the default, is usually what keeps a fleet inside the limit. Atlas also terminates idle connections, so set maxIdleTimeMS below that period.
Serverless functions repeat the SQL-side problem in a MongoDB shape: each execution environment holds a client, and each client holds a pool per server. Create the client at module scope, set maxPoolSize to a small number, and never call client.close() at the end of a handler. The full pattern is in Sizing MongoDB Connection Pools for Serverless Functions.
Load balancers and mongos. In a sharded deployment the driver opens a pool per mongos it is aware of, so adding routers multiplies connections. When connecting through a load balancer in load-balanced mode, the driver keeps one pool for the balancer address instead — which changes the arithmetic but also removes the driver’s ability to spread load itself.
No SQL-style proxy exists. There is no MongoDB equivalent of PgBouncer in common use; the pool and the deployment’s limit are the only controls, which makes client-side sizing more important than it is in the PostgreSQL world, where PgBouncer can absorb an oversized fleet.
| Deployment | Pools per client | Main risk | Control |
|---|---|---|---|
| Replica set | one per member | Fleet total across members | maxPoolSize, fewer processes |
Sharded with several mongos |
one per router | Connections multiply with routers | maxPoolSize, srvMaxHosts |
| Load-balanced mode | one per balancer address | Balancer becomes the bottleneck | Balancer capacity, maxPoolSize |
| Serverless functions | one per environment | Environment count | Small pool, concurrency cap |
Read preference, transactions and where connections go
Two features decide which pool an operation uses, and both change the connection arithmetic in ways that are easy to miss.
Read preference routes operations to different pools. With the default primary, essentially all traffic uses the primary’s pool and the secondaries’ pools stay nearly empty — which means a maxPoolSize sized for total load is effectively a limit on primary connections alone. With secondaryPreferred, reads spread across the secondary pools, so the same total load produces a lower peak per pool and survives a primary election better. The trade is staleness: a secondary may lag, and read-your-writes is not guaranteed unless the read uses the primary or a causally consistent session.
// Reads that tolerate lag, spread across secondaries
const catalog = client.db("shop", { readPreference: "secondaryPreferred" });
// Reads that must see the session's own writes
const session = client.startSession({ causalConsistency: true });
Transactions hold a connection for their whole duration. A multi-document transaction pins one connection from startTransaction to commitTransaction, including any application work in between, and every operation inside it goes to the same server. Transactions therefore consume pool capacity in proportion to their wall-clock duration, not their operation count:
connections_held_by_transactions = transactions_per_second × mean_transaction_seconds
At 50 transactions per second with a mean duration of 120 ms — because an external call sits inside the transaction — that is 6 connections permanently occupied, which on a pool of 11 is more than half the capacity. Moving the external call outside the transaction typically reduces the duration to a few milliseconds and the occupancy with it.
Sessions are cheap; transactions are not. Explicit sessions used only for causal consistency do not hold connections between operations; only an open transaction does. That makes causal consistency a much cheaper way to get read-your-writes than wrapping reads and writes in a transaction.
| Feature | Which pool | Connection cost |
|---|---|---|
Default primary reads |
primary only | All load on one pool |
secondaryPreferred reads |
secondary pools | Spreads load; tolerates lag |
| Causally consistent session | follows read preference | No connection held between operations |
| Multi-document transaction | one server, pinned | One connection for the whole transaction |
$out / $merge aggregation |
primary | Long-running; holds a connection |
Operational Boundary: Read preference and transactions determine which pool and for how long. They do not change maxPoolSize, which remains a per-server, per-client limit.
Common failure patterns & remediation
| Symptom | Root cause | Exact fix | Validation command |
|---|---|---|---|
MongoWaitQueueTimeoutError / Timed out while checking out a connection |
Pool saturated or establishment throttled | Size maxPoolSize from operations in flight; check maxConnecting |
Count connectionCheckOutFailed by reason |
| Connection count far above expectation | Default maxPoolSize × servers × processes |
Set maxPoolSize explicitly |
db.serverStatus().connections.current |
| Burst of errors at every failover | Pool cleared, refilling at maxConnecting |
Raise maxConnecting modestly; ensure retryable reads and writes are enabled |
connectionPoolCleared event count |
totalCreated climbing steadily |
Churn from idle timeouts or repeated clears | Align maxIdleTimeMS with network timeouts |
serverStatus().connections.totalCreated rate |
| Client per request in a function | New topology scan and pool per invocation | Module-scope client; no close() in the handler |
Connection count versus concurrency |
MongoServerSelectionError before any checkout |
Topology or network, not the pool | Check DNS, TLS, IP access lists and serverSelectionTimeoutMS |
Driver heartbeat events |
Frequently Asked Questions
Does maxPoolSize apply to the whole cluster?
maxPoolSize = 20 allows up to 100 pooled connections from that one client, even though most operations go to the primary.Should minPoolSize be raised to avoid slow first operations?
Is maxConnecting worth raising?
How do retryable reads and writes interact with pool errors?
Does maxIdleTimeMS need to be set explicitly?
How many connections do the monitoring threads use?
maxPoolSize but inside the deployment’s connection limit, which is why the fleet arithmetic on this page adds them separately.Related
- Pool Architecture & Algorithm Fundamentals — the parent overview of pool topology and lifecycle.
- Tuning MongoDB maxPoolSize and waitQueueTimeoutMS — sizing and bounding checkout in detail.
- Diagnosing MongoDB Connection Storms After Failover — what pool clearing does and how to survive it.
- Monitoring MongoDB Driver Pool Events — turning CMAP events into metrics and alerts.
- Connection Pool Sizing Formulas — the queueing arithmetic used above.