Drivers, connection pools, timeouts and retries
Configure the application side correctly: one client per process, pool sizing, server selection and timeouts, retryable writes, and avoiding connection storms.
- Size connection pools and compute total connections
- Set timeouts that fail fast instead of hanging
- Use retryable writes and reads
- Avoid connection storms in serverless and autoscaling environments
conceptThe concept
- Create one MongoClient per process and reuse it. It holds a pool per server plus monitoring connections.
maxPoolSize(default 100),minPoolSize(0),maxIdleTimeMS,maxConnecting(2, limits simultaneous connection establishment),waitQueueTimeoutMS.serverSelectionTimeoutMS(30 s default): how long to wait for a suitable server (e.g. during an election).connectTimeoutMSandsocketTimeoutMS; newer drivers support a single client-side operation timeout (timeoutMS).retryWrites=true(default) retries a failed single write once on a new primary;retryReads=truedoes the same for reads.- Total server connections = app instances x pool size per instance (per mongos/member) + monitoring connections. With 200 pods x 100 = 20,000 potential connections per member.
whyWhy it matters
Connection storms are a classic outage: an autoscaler adds 300 pods, each opens up to 100 connections, the server spends its CPU on authentication handshakes (SCRAM is deliberately expensive) and memory on connection threads, and everything slows down, causing more scaling. Bad timeouts turn a 10 s failover into minutes of hung requests.
Use caseReal-world scenario
A service with 40 pods runs ~30 concurrent DB operations per pod at peak. Setting maxPoolSize=40, minPoolSize=5, maxConnecting=2, maxIdleTimeMS=60000 caps the cluster at 1,600 connections per member. serverSelectionTimeoutMS=5000 and an operation timeout of 2 s make requests fail fast during incidents, and retryable writes absorb failovers.
Try itExamples
Run and edit these in the interactive lesson.
// create once at startup, share everywhere
const client = new MongoClient(process.env.MONGO_URI, {
maxPoolSize: 40,
minPoolSize: 5,
maxConnecting: 2,
maxIdleTimeMS: 60_000,
serverSelectionTimeoutMS: 5_000,
timeoutMS: 2_000, // client-side operation timeout (newer drivers)
retryWrites: true,
retryReads: true,
appName: "orders-api", // shows up in currentOp and logs
});
await client.connect();
export const db = client.db("shop");let client; // module scope survives warm invocations
export async function handler(event) {
client ??= await new MongoClient(process.env.MONGO_URI, { maxPoolSize: 5 }).connect();
return client.db("shop").collection("orders").findOne({ orderNo: event.orderNo });
}db.serverStatus().connections
db.currentOp({ appName: "mongosh" }).inprog.lengthinternalsUnder the hood
Each server connection uses a thread on mongod (or a pooled ingress thread with newer async networking) and roughly 1 MB of memory. Drivers monitor each server with a dedicated connection (and an RTT connection in newer drivers), using hello every heartbeatFrequencyMS (10 s) or immediately after an error. Retryable writes use a transaction number in a logical session so the server can detect and ignore a duplicate retry.
Avoid / preferDo this, not that
app.get("/orders", async (req, res) => {
const client = new MongoClient(uri)
await client.connect()
// ...
})const client = new MongoClient(uri) // once, at startup
await client.connect()
app.get("/orders", async (req, res) => { /* use client */ })A client per request means a TCP and TLS handshake and authentication every time, and connection storms under load. One client per process shares a pool.
new MongoClient(uri) // default timeouts everywhere
new MongoClient(uri, { maxPoolSize: 50, serverSelectionTimeoutMS: 5000, connectTimeoutMS: 5000 })
// plus maxTimeMS on queries that must not run longExplicit pool size and timeouts make failures fast and visible instead of piling up waiting requests.
PitfallsCommon mistakes
- Creating a new MongoClient per request.
- Huge default pools multiplied by hundreds of pods.
- No timeouts: requests hang for 30 s+ during elections or network issues.
- Catching and silently retrying all errors in application code, duplicating non-idempotent writes.
- Not setting
appName, making it impossible to tell which service owns which connections.
LimitsWhen not to use it
Raising maxPoolSize is not a fix for slowness. Every connection costs server memory and a thread; twenty pods with a pool of 500 each can open 10,000 connections. Find the slow operations holding connections first.
SecuritySecurity notes
Use tls=true in production URIs, load credentials from a secrets manager, and never log the connection string: it often contains the password.
ChallengeTry it yourself
From serverStatus().connections, show current and available connections and the percentage of the limit in use.
Show one solution
const c = db.serverStatus().connections
printjson({ current: c.current, available: c.available, usedPct: +(100 * c.current / (c.current + c.available)).toFixed(3) })InterviewQuestions you should be able to answer
How do you size a connection pool?
From concurrency, not from defaults: peak concurrent operations per instance (Little's law: throughput x latency) plus some headroom. Multiply by instance count and check against server limits and memory. Use maxConnecting to avoid storms and minPoolSize to avoid cold-start latency. Monitor wait queue time in the driver and connection counts on the server.
What are retryable writes?
Drivers automatically retry certain write operations once after a network error or a "not primary" error, after selecting a new primary. The server uses the session id and transaction number to ensure the write is applied only once. Multi-document writes like updateMany are not retryable as a whole; single-document writes and bulk writes of retryable operations are.
QuizCheck your understanding
4 questions with instant feedback in the interactive lesson; a wrong answer goes into your review deck.