Cloudflare/workerd: sql.reserve() never resolves on a pool that has not yet opened a connection
Nobody has claimed this yet.
- Dominant language
- JavaScript
- Stars
- 8.7k
- Forks
- 374
- Avg merge
- 11d 16h
- Merged PRs (30d)
- 1
Description
Summary
On the Cloudflare Workers build (cf/), sql.reserve() never resolves if the pool has not yet opened a connection. It does not reject or time out — the returned promise simply stays pending forever.
Once any query has run on the pool, reserve() works normally and keeps working, because releasing returns the connection to open.
This bites anything that reserves as its first act. kysely-postgres-js acquires connections with postgres.reserve(), so the very first Kysely query against a fresh client hangs indefinitely.
Version: postgres@3.4.9, workerd via @cloudflare/vitest-plugin (also reachable through wrangler dev).
Reproduction
import postgres from 'postgres'
// Cold pool — hangs forever.
const cold = postgres(connectionString, { max: 1, fetch_types: false })
await cold.reserve() // never settles
// Same pool, after one query — fine.
const warm = postgres(connectionString, { max: 1, fetch_types: false })
await warm`SELECT 1`
await warm.reserve() // resolves immediately
Observed with max: 1 and max: 2 alike, so it is not pool-size exhaustion.
Where it seems to come from
reserve() in cf/src/index.js:
const c = open.length
? open.shift()
: await new Promise((resolve, reject) => {
const query = { reserve: resolve, reject }
queries.push(query)
closed.length && connect(closed.shift(), query)
})
On a cold pool open is empty, so it queues a waiter whose only chance of being driven is the closed.length && connect(...) on the same line. If nothing is sitting in closed at that moment — or if connect() does not carry the queued reserve waiter through to resolution on this runtime — nothing ever resolves it. The socket polyfill's connect() is async and dynamically imports cloudflare:sockets, so connection setup here does not make synchronous progress the way it does on Node, which may be the relevant difference.
I have not traced it far enough to propose a patch with confidence, so this is a report rather than a PR — happy to dig further if the diagnosis above looks like it is pointing at the right place.
Workaround
Run one trivial query before handing the pool to anything that reserves:
const sql = postgres(connectionString, { max: 1, fetch_types: false })
await sql`SELECT 1`
Related: #1202 (unhandled rejection on sql.end() in the same build).
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in cf/src/index.js at the reserve() implementation and trace how the queued waiter is handled by connect(), including the socket polyfill path described in the report. Run the cold-pool reproduction with max 1 and max 2, then verify that reserve() settles on a fresh pool without requiring a preliminary query.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, postgres
- Domain
- databases
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 68/100