Backend

Fix a cached database connection that never recovers

Kamruz
Kamruz
Oct 14, 2026
5 min read
Fix a cached database connection that never recovers

The first request after startup cannot reach the database. The helper stores the rejected connection promise in a module-level variable. The network recovers, but every later request awaits the same rejected promise and fails immediately. Restarting the process makes the application work, creating the impression that the driver itself never reconnects. The application cache actually preserved a failed initialization forever.

This example assumes the MongoDB Node driver and a long-lived Node runtime. Driver versions and hosting lifetimes matter. A reusable client owns a connection pool; it is not equivalent to one permanently open socket. The helper should manage initialization carefully without replacing the driver's own topology and pool behavior.

Distinguish initialization from query failure

Record whether the failure occurs while constructing or connecting the client, while selecting a server for an operation, or while executing a query. A cached rejected initialization promise needs a different repair from an invalid query or expired credential. Do not close the client after every exception.

Inspect module-level and global caches. A common pattern stores both a connected client and an in-flight promise to prevent duplicate initialization. The promise must be cleared when that attempt fails. Otherwise later callers cannot start a new attempt even after conditions change.

Use safe logs containing stage, error category, and attempt identifier. Do not print the connection string. Ask an AI assistant to trace the cache assignments and identify which states are reachable after rejection. That is a precise code question; it does not require production credentials.

Model the cache states

The helper has no client, initialization in progress, or reusable initialized client. Concurrent requests can share the in-progress attempt. A failed attempt should return the cache to a state where a later bounded attempt is possible. A successful attempt should preserve the reusable client.

Do not label a client healthy forever merely because connect once succeeded. Later operations can fail due to network or server conditions, and the driver may recover through its own monitoring. The application should expose operation failures accurately while allowing the driver to manage its pool.

A shutdown path is separate. When the process intentionally terminates, close the client according to the application's lifecycle. Closing on each request defeats pooling and can create avoidable connection churn. In serverless environments, module reuse is opportunistic rather than guaranteed, so avoid promising one global client across all instances.

Clear only the failed attempt

The following outline captures the initialization promise and clears it only if the cache still refers to that attempt. This protects against a later attempt being overwritten by an older failure handler.

let client: MongoClient | undefined
let pending: Promise<MongoClient> | undefined

export async function getClient() {
if (client) return client
if (!pending) {
const candidate = new MongoClient(databaseUri)
let attempt: Promise<MongoClient>
attempt = candidate.connect().then(() => {
client = candidate
return candidate
}).catch(async error => {
if (pending === attempt) pending = undefined
await candidate.close().catch(() => {})
throw error
})
pending = attempt
}
return pending
}

This is a minimal pattern to review against the installed driver's interface and application lifecycle, not a complete configuration module. Validate databaseUri safely before use. A deliberate close failure is suppressed only while cleaning a failed candidate; application query failures should not be broadly swallowed.

The explicit promise type avoids an inference problem caused by referencing the attempt in its own failure callback. Keep that language-level detail distinct from the connection-state invariant. Compile the pattern in the real project and provide the actual driver import and validated configuration.

Bound attempts and waiting

Configure server-selection and connection behavior using the driver's documented options for its version. Choose timeouts from the request's acceptable wait, not arbitrary large values. A request that waits longer than the platform deadline may never deliver its intended error response.

Clearing a rejection permits recovery, but many simultaneous requests during an outage can trigger repeated attempts after each failure. Consider a short bounded backoff or circuit state if evidence shows retry storms. Keep one shared in-flight attempt and expose a safe unavailable response rather than creating a client per caller.

Connection-pool size is a separate capacity decision. Increasing it does not fix a cached rejection or invalid credentials. Measure operation concurrency and wait behavior before changing resource limits. The driver documentation describes pool options and should be checked for version-specific behavior.

Verify recovery without restarting

Start one process with a controlled unavailable database endpoint or isolated network failure. Make concurrent calls and confirm they share one initialization attempt. Restore connectivity, call again, and verify a new attempt succeeds in the same process.

After success, issue several operations and verify the client is reused. Then simulate a later query failure and confirm the helper does not create a new client for every error. The driver may recover depending on the failure; the test should distinguish driver reconnection from application cache reset.

Use a fake connector for deterministic cache-state tests and an isolated real database for the actual driver recovery boundary. A stub that resolves on its second call proves the helper permits retry; it does not prove a particular network topology recovers. State both scopes in the validation evidence.

Review surrounding deployment assumptions

Check credentials, DNS, TLS configuration, allowlists, and runtime compatibility. A correct retry pattern cannot recover from a permanently incorrect URI or unsupported runtime. The initial failure may have exposed two problems: configuration and rejection caching. Fix both based on evidence.

In development, hot reload can create multiple module instances or global caches. Use the framework's established project pattern and inspect actual client counts. Do not add a global property solely because an assistant suggests it; understand whether it persists within the intended runtime and whether tests reset it correctly.

Deployment checks cover configuration boundaries. N plus one diagnosis covers avoidable operation volume that can make pool pressure look like connection failure.

Keep failure evidence honest

Ask an AI reviewer to enumerate states after success, rejection, concurrent callers, and shutdown. Require it to identify which client is closed and which promise remains cached. A broad claim that caching database connections is best practice does not answer those questions.

Record the observed recovery path and remaining topology assumptions. The repair is successful when a transient initialization failure no longer poisons the process while healthy clients remain reusable. It should not be reported as a guarantee that every database outage becomes invisible to users.

Distinguish process recovery from request replay

A recovered connection does not make a failed write safe to replay automatically. The first attempt may have reached the database before the network error became visible. Use the operation’s idempotency or conditional-write contract to decide whether another attempt is appropriate. Keep connection initialization retry separate from business-operation retry. Test an initialization failure, a confirmed rejected write, and an ambiguous write outcome as different cases. This prevents a connection-helper repair from introducing duplicate records through an unrelated blanket retry wrapper.

Sources

About the Author

Kamruz

Kamruz

Full Stack Developer

A passionate developer who loves creating modern web applications and sharing knowledge with the community.

More About Me

Related Articles