Node.js
node-postgres — Client already connected and released-client reuse
Written and reviewed by Sahil Srivastav
Error: Client has already been connected. You cannot reuse a client.What this error actually means
A pg Client represents one connection lifecycle. Calling connect again on a client that has already started or completed connection is not how to reconnect or borrow it again. A Pool is the reusable manager; pool.connect returns a client already connected and temporarily assigned to the caller.
Release is an ownership transfer, not a disconnection request. After client.release(), the pool may give that same physical client to another request. Continuing to use the old reference can mix operations from different logical owners. It may not fail immediately with a helpful exception, which makes use-after-release more dangerous than the explicit already-connected error.
A related guard is “Release called on client which has already been released to the pool.” That identifies duplicate cleanup. These failures share a lifecycle misunderstanding, but the repairs differ: do not reconnect checked-out clients, do not use them after release, and ensure precisely one path returns each checkout.
Causes, most common first
- 1Client and Pool are treated as interchangeable. A module exports one Client and every handler calls connect on it. Alternatively, a handler obtains a pooled client and calls connect again. Use one long-lived Pool for repeated independent requests, or deliberately own one standalone Client from connect through end.
- 2Release occurs before async work has completed. A callback starts queries without awaiting them and an outer finally returns the client immediately. A transaction helper may return a promise without awaiting it inside try/finally, allowing cleanup to run before that promise settles. The code looks structured while its ownership interval is too short.
- 3Multiple layers believe they own cleanup. A helper releases the client on failure and the caller also releases in finally. Helpers that receive a borrowed client should normally perform queries without taking over its lifetime. Make ownership visible in the API instead of relying on every caller remembering an undocumented exception.
- 4A checked-out client escapes into detached work. The request stores the client in a singleton, timer or background callback. By the time that work executes, the checkout is over. Pass the pool or operation data to a new independent task rather than sharing a borrowed connection across unrelated lifetimes.
When you see it
- The second request fails after a singleton Client was connected per request
- Calling connect on a client returned by pool.connect fails immediately
- A finally block reports duplicate release after a catch also released the client
- Queries appear in the wrong transaction after a callback continues past release
How to diagnose it
Step 1
Identify the object that receives connect
Trace construction and imports: new Client and new Pool establish different ownership contracts. Search connect, release and end together, including wrapper helpers. A client returned by pool.connect must not receive another connect call.
rg -n "new (Client|Pool)|\.connect\(|\.release\(|\.end\(" srcStep 2
Log checkout identity and completion order
Assign a safe operation ID at acquisition and log acquisition, transaction completion and release. Look for queries after release and for two release events with the same checkout ID. Do not log connection strings or query parameters containing credentials.
Step 3
Inspect pool occupancy during failure
Record pool.totalCount, idleCount and waitingCount from the affected instance. A lifecycle error can coexist with a leak: fixing duplicate connect calls does not prove every checkout returns. After test requests settle, waitingCount should clear and idle clients should become available.
Step 4
Force an error between BEGIN and COMMIT
Verify rollback uses the same client, cleanup happens once, and another request can subsequently borrow a healthy connection. Also delay a query deliberately to detect early release that ordinary fast tests conceal.
The fix
Use pool.query for a single independent query so the pool owns acquisition and release. For a transaction, acquire once with pool.connect and run BEGIN, every statement, COMMIT or ROLLBACK on that same client. Separate pool.query calls are not a transaction because they may use different sessions.
Release exactly once in finally after all client work has settled. Await a promise before leaving a try whose finally releases its resources. Make borrowed-client helpers return their work and never release a resource owned by the caller.
If rollback fails because the connection is broken, discard that client rather than returning uncertain session state to the pool. Preserve the original operation error for diagnosis while recording the rollback failure separately. Do not let cleanup overwrite the only evidence of the initiating failure.
At process shutdown, stop admitting new requests, allow current checkouts to finish, then await pool.end. Do not call pool.end at the end of each request. A standalone Client that has ended should be replaced with a new Client if a new independent connection is required.
async function inTransaction(pool, work) {
const client = await pool.connect();
let discard = false;
try {
await client.query("BEGIN");
const result = await work(client);
await client.query("COMMIT");
return result;
} catch (error) {
try {
await client.query("ROLLBACK");
} catch (rollbackError) {
discard = true;
console.error("rollback failed", rollbackError);
}
throw error;
} finally {
client.release(discard);
}
}How to stop it coming back
- Document whether each helper borrows, owns or merely receives a client
- Assert cleanup once and no queries after release in transaction failure tests
- Keep clients out of global request state and background task payloads
- Monitor checkout waiters and durations alongside database session state
FAQ
Can I call client.connect after pool.connect?
No. The pool has already established the connection before handing it out. Use the returned client for the checkout interval, then release it once.
Why does a query sometimes work after release?
Release returns ownership to the pool; it does not necessarily close the socket. The stale reference may still address a live client now owned by another request. Apparent success is not evidence of correct isolation.
Can I use pool.query inside a transaction helper?
Use the checked-out transaction client for all statements that belong to that transaction. pool.query may select another connection, so that statement can run outside the intended transaction even if the SQL text looks correct.
Related
Other errors engineers hit next to this one
- RuntimeError: Event loop is closed
- Task was destroyed but it is pending!
- Executing <Handle ...> took 2.418 seconds (blocked event loop)
- SettingWithCopyWarning: A value is trying to be set on a copy of a slice
- celery.exceptions.WorkerLostError: Worker exited prematurely
- requests.exceptions.ReadTimeout: HTTPSConnectionPool read timed out
- UnicodeDecodeError: 'utf-8' codec can't decode byte
- AssertionError: daemonic processes are not allowed to have children