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, per MongoClient. The fleet total is clients × 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 waitQueueTimeoutMS in older drivers and by the unified timeoutMS in current ones; when neither is set, a checkout can wait for the full serverSelectionTimeoutMS.
  • 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.
MongoClient pools across a replica set A single MongoClient in one application process holds three separate connection pools, one for each replica set member, each bounded by maxPoolSize. Twelve application processes therefore open up to thirty-six pools. Application process one MongoClient Driver topology monitor one pool per server pool → primary · maxPoolSize 100 pool → secondary A · maxPoolSize 100 pool → secondary B · maxPoolSize 100 12 processes × 3 servers × 100 = up to 3,600 connections
One MongoClient holds a pool for every server it discovers. The connection total the database sees is the product of clients, servers and maxPoolSize.

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.

Pool warming with maxConnecting After a pool clear, fifty waiting operations are served as connections are established two at a time, so the pool refills gradually rather than opening fifty sockets at once. Connections established after a pool clear (maxConnecting = 2) two at a time, as each completes maxPoolSize clear +8 s raising maxConnecting speeds recovery and increases the load spike on a recovering server
maxConnecting throttles establishment, so a cold or cleared pool warms gradually. That is protection against storms and also the reason recovery after a failover is not instant.

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.

Default versus sized MongoDB pool across a fleet Twelve processes against a three-member replica set hold 3,600 connections at the default maxPoolSize of 100, against a measured requirement of 9 per pool, which totals 324. Pooled connections, 12 processes × 3 servers maxPoolSize 100 3,600 maxPoolSize 9 324 operations in flight per server: about 6 at peak the rest of the default pool is capacity the deployment must reserve but never uses
The default pool is sized for a much larger workload than most services run. Sizing it from operations in flight frees connections the deployment's limit has to accommodate.

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.

MongoDB checkout failure reasons and their causes A failed checkout with reason timeout while the pool is full indicates saturation; with connections being created it indicates slow establishment; a connectionError reason indicates the connection itself failed; server selection errors occur before checkout and indicate topology problems. Checkout failed or operation timed out reason field on the event timeout · pool at maxPoolSize: saturated timeout · creating: slow establishment connectionError: TLS, auth or network server selection: topology, not the pool selection happens before checkout
The reason attached to a failed checkout separates the three causes that look identical from the application's point of view.

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?
No, and this is the most common misreading. It applies per server the client knows about. A five-member replica set with 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?
A small minimum — two or three — removes cold-start latency for the primary’s pool. Remember it is per server and per process, so a large minimum across a fleet holds many permanently open connections that the deployment’s limit must accommodate.
Is maxConnecting worth raising?
Raise it only when recovery after failover is measurably too slow and the server can absorb a faster reconnect. Values of 4 or 5 halve warm-up time; much higher values recreate the connection storm the setting exists to prevent.
How do retryable reads and writes interact with pool errors?
A checkout timeout is not retried by the driver’s retryable-writes machinery, because no operation was ever sent. Errors that occur after an operation reaches the server — including those caused by a failover — are retried once by default, which is why enabling retryable reads and writes noticeably reduces failover impact.
Does maxIdleTimeMS need to be set explicitly?
Yes, in almost every deployment. The default of 0 means connections are never closed for being idle, so a pool that grew during a peak keeps those connections indefinitely, and any network device that silently drops idle flows leaves dead sockets in the pool. A value below the shortest idle timeout on the path — the load balancer, the NAT gateway or the cluster’s own limit — keeps the pool honest.
How many connections do the monitoring threads use?
Roughly two per server per client with modern streaming monitoring: one for the heartbeat stream and one for round-trip time measurement. They are outside maxPoolSize but inside the deployment’s connection limit, which is why the fleet arithmetic on this page adds them separately.