drizzle-team / drizzle-team/drizzle-orm

[BUG]: Pooled transaction() leaks the pool client when BEGIN fails (node-postgres / neon-serverless / vercel-postgres)

Open
#6,241 1 comment 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
35.8k
Forks
1.6k
Avg merge
2d 7h
Merged PRs (30d)
4

Description

**drizzle-orm:** 0.45.2 — same code on `main` (0.45.3) and `beta` (1.0.0-rc.4)
**drizzle-kit:** 0.31.10
**Other packages:** pg 8.22.0

### Describe the Bug

If `db.transaction()` runs on a `Pool` and the `BEGIN` statement fails, the client taken from the pool is never released.

`begin` is executed before the `try/finally` that calls `release()`:

```ts
// drizzle-orm/src/node-postgres/session.ts
// neon-serverless/session.ts and vercel-postgres/session.ts have the same shape
const session = isPool ? new NodePgSession(await this.client.connect(), ...) : this;
const tx = new NodePgTransaction(...);
await tx.execute(sql`begin...`); // throws here → the finally below never runs
try {
const result = await transaction(tx);
await tx.execute(sql`commit`);
return result;
} catch (error) {
await tx.execute(sql`rollback`);
throw error;
} finally {
if (isPool) session.client.release();
}
```

`BEGIN` does fail in practice: `statement_timeout` (`57014`), a connection dropped right after connect (`57P01`, "Connection terminated unexpectedly"), admin shutdown. Each time, the pool loses one client for good. Under load `pool.connect()` eventually hangs on an empty pool.

Repro without a database — subclass `pg.Pool` and make `BEGIN` throw:

```ts
import pg from 'pg';
import { drizzle } from 'drizzle-orm/node-postgres';

class BeginFailurePool extends pg.Pool {
released = 0;
async connect() {
return {
query: async (q: string | { text: string }) => {
const text = typeof q === 'string' ? q : q.text;
if (text.toLowerCase().startsWith('begin')) {
throw Object.assign(new Error('canceling statement due to statement timeout'), { code: '57014' });
}
return { rows: [], rowCount: 0 };
},
release: () => { this.released++; },
} as any;
}
}

const pool = new BeginFailurePool({ connectionString: 'postgres://u:p@localhost/db' });
await drizzle({ client: pool }).transaction(async () => {}).catch(() => {});
console.log(pool.released); // 0 — expected 1
```

Expected: the client is released on every exit path, including a failing `BEGIN`. Moving `begin` inside the `try` is enough: the existing `catch` runs `rollback` (only a WARNING when no transaction is open) and `finally` releases. The error the caller sees stays the same `DrizzleQueryError`.

I have the fix plus a driver-mocked test for the three sessions; PR follows.

Contributor guide

Open the contributing guide

Research direction

Start with the transaction implementations in drizzle-orm/src/node-postgres/session.ts, neon-serverless/session.ts, and vercel-postgres/session.ts, then inspect the driver-mocked tests mentioned in the issue. Verify that a failing BEGIN releases the pooled client for all three sessions, preserves the caller-visible DrizzleQueryError, and passes the relevant test suite.

Written by the indexing model from the issue text.

Assessment

Tech stack
nodejs, postgres, typescript
Domain
databases
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.