Node.js
Node.js — Sequelize or Knex times out acquiring a database connection
Written and reviewed by Sahil Srivastav
Knex: Timeout acquiring a connection. The pool is probably full. Are you missing a .transacting(trx) call?What this error actually means
The ORM or query builder waited for a connection and exceeded its acquisition budget. The waiting query may never have reached the database. Sequelize can report SequelizeConnectionAcquireTimeoutError; Knex includes the transaction-context hint shown above. Neither message establishes that the database needs a larger connection limit.
A pool has a finite number of checkouts. Each checkout is occupied for the entire borrow interval, which can include SQL, application computation and unrelated HTTP requests. Waiting grows when arrival rate exceeds release rate, when owners leak connections, or when current owners wait for new borrowers that cannot acquire a slot.
The last case is particularly deceptive. Suppose every pool connection belongs to a transaction, and each transaction awaits a helper that issues a query through the root knex object. Those helpers request additional connections while the transactions retain the existing ones. The database can be nearly idle even though every caller times out.
Causes, most common first
- 1A query escapes its transaction context. Knex helpers use knex(table) rather than trx(table), or omit transacting(trx). Sequelize helpers omit the explicit transaction option where automatic context is not configured or does not apply. Nested acquisition can consume all capacity without expensive SQL being involved.
- 2Transactions span slow external work. The application opens a transaction, calls a remote API, then writes the result. The connection remains occupied throughout the remote delay. An upstream latency incident can therefore exhaust the database pool even though database performance has not changed.
- 3A transaction never completes on one branch. Unmanaged transactions must commit or roll back on every path. Managed callbacks still need their promises returned or awaited correctly. A forgotten await can let the lifecycle close too early; a never-settling promise can hold it indefinitely.
- 4Slow or blocked SQL creates genuine saturation. A query waits behind a lock, scans far more rows after data growth, or runs too many times per request. Replica count also matters: ten processes each configured for twenty connections can overwhelm a database sized for the previous deployment.
When you see it
- Errors appear after a delay equal to the pool acquisition timeout
- Increasing concurrent requests reduces throughput instead of increasing it
- Database sessions are idle in transaction while application tasks wait
- The problem begins after a helper stops receiving the transaction object
How to diagnose it
Step 1
Separate acquisition wait from query execution
Record time before acquisition, time after checkout and time after release. Instrument the application boundary if the ORM does not expose stable public metrics for the installed version. A single total query timer cannot distinguish waiting for a slot from running SQL.
Step 2
Inspect what database sessions are doing
For PostgreSQL, active queries waiting on locks suggest contention; old idle-in-transaction sessions suggest application ownership problems. Restrict the query to the application role or application_name when several services share the database.
SELECT pid, application_name, state, wait_event_type, wait_event,
now() - xact_start AS transaction_age,
now() - query_start AS query_age
FROM pg_stat_activity
WHERE datname = current_database() AND pid <> pg_backend_pid()
ORDER BY xact_start NULLS LAST;Step 3
Audit transaction propagation across helper calls
Start at the outer transaction and follow every awaited function. Look for root-level query-builder calls or missing Sequelize transaction options. Reproduce with a deliberately small pool in staging: context escapes often become deterministic when only one slot exists.
Step 4
Compare recovery after traffic stops
If connections become available as requests finish, investigate holding time and capacity. If the pool stays exhausted after legitimate work should have ended, locate leaked or permanently waiting owners. Restarts clear both conditions and therefore do not distinguish them.
The fix
Pass transaction ownership explicitly through the call graph. In Knex, query via the transaction object for all participating statements. In Sequelize, use the transaction option consistently unless the configured context mechanism is verified by tests. A helper that needs an independent transaction should make that choice explicit.
Move network calls and expensive transformations outside the transaction whenever the consistency contract permits. If a remote side effect must follow a committed change, use a durable outbox or another explicit workflow rather than holding a database connection across an unreliable network.
Make commit and rollback lifecycle complete on success, failure and cancellation. Keep acquisition deadlines below the caller’s remaining deadline, and bound request concurrency so timed-out callers do not continue accumulating in an invisible queue.
For measured SQL saturation, fix blocking or query plans first, then size the total pool budget across all processes with database headroom. Extending the acquire timeout stores more waiting work; increasing pool size may move the queue into the database and worsen tail latency.
// One transaction connection is used throughout the call graph.
async function reserve(knex, itemId, userId) {
return knex.transaction(async trx => {
const item = await trx("items")
.where({ id: itemId }).forUpdate().first();
if (!item || item.available < 1) throw new Error("unavailable");
await trx("items").where({ id: itemId }).decrement("available", 1);
await insertReservation(trx, itemId, userId);
});
}
async function insertReservation(trx, itemId, userId) {
await trx("reservations").insert({ item_id: itemId, user_id: userId });
}How to stop it coming back
- Measure checkout duration and acquisition wait as separate distributions
- Run transaction helpers against a small pool to expose nested acquisition
- Budget database connections across replicas, workers and maintenance jobs
- Alert on waiting borrowers before the acquisition timeout rate rises
FAQ
Should I increase acquireConnectionTimeout?
Only if a measured, acceptable queue needs a larger budget within the caller’s deadline. It does not release connections or fix a transaction waiting on its own pool. A longer queue can increase retained request memory and delay failure.
Why is PostgreSQL idle when the pool is full?
Application code can hold a connection without running SQL. It may be waiting on HTTP, a missing promise completion or another pool acquisition. Inspect transaction age and the application call graph together.
Why did scaling Node replicas make this worse?
Each process usually owns its own pool. More replicas multiply potential database connections and concurrent queries. Keep a deployment-wide budget rather than treating each process’s pool maximum as an independent capacity decision.
Related
Other errors engineers hit next to this one
- command not found in a script that works interactively
- Permission denied when executing a script
- bad interpreter: No such file or directory with CRLF
- Argument list too long
- Too many open files
- Out of memory: Killed process
- No space left on device despite free disk space
- Text file busy during executable replacement