sql: grow connection pool lazily instead of opening max on first use#30636
sql: grow connection pool lazily instead of opening max on first use#30636robobun wants to merge 3 commits into
Conversation
|
Updated 1:40 PM PT - Jun 15th, 2026
❌ @alii, your commit 5143f2c has some failures in 🧪 To try this PR locally: bunx bun-pr 30636That installs a local version of the PR into your bun-30636 --bun |
|
Found 2 issues this PR may fix:
🤖 Generated with Claude Code |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughBoth MySQL and Postgres adapters now grow connection pools lazily on-demand up to a new ChangesLazy Connection Pool Growth
Possibly related issues
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@test/js/sql/sql-mysql.test.ts`:
- Around line 17-76: Extract the two tests beginning with describe("mysql
connection pool grows lazily (`#30632`)") from test/js/sql/sql-mysql.test.ts into
a new test file named something like test/js/sql/sql-mysql-nondocker.test.ts (or
sql-mysql-mock.test.ts); remove that describe block from the original file so
Docker-gated suite remains clean; keep the tests' exact bodies and using/server
setup and preserve imports (e.g., SQL, Bun.listen) and async semantics, and
ensure the new file is picked up by the test runner (naming convention) so the
non-Docker/mock MySQL regression suite runs separately from sql-mysql.test.ts.
In `@test/js/sql/sql.test.ts`:
- Around line 112-114: The test currently uses a fixed setTimeout to "give
connectors a chance" which is flaky; replace the hardcoded wait with a
deterministic wait that polls the condition until openedSockets === 0 (or times
out); update the snippet around openedSockets so the test awaits a
small-interval retry loop or a test helper (e.g., waitFor) that repeatedly
checks openedSockets and resolves when it becomes 0, failing the test if a
configurable timeout elapses—target the openedSockets variable and the test
block where the setTimeout is used.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 652c6181-90bd-4cf5-ad8e-ed0d16dd92da
📒 Files selected for processing (4)
src/js/internal/sql/mysql.tssrc/js/internal/sql/postgres.tstest/js/sql/sql-mysql.test.tstest/js/sql/sql.test.ts
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@test/js/sql/sql-pool-lazy-growth.test.ts`:
- Around line 45-46: The test currently iterates adapters with a for-loop and
calls describe(`${adapter} connection pool grows lazily (`#30632`)`, ...) which
violates the guideline to use parameterized suites; replace the manual loop with
describe.each([...]) so the suite is declared via
describe.each(["postgres","mysql"]) and the suite title uses the adapter
parameter (e.g., describe.each(...)(`%s connection pool grows lazily (`#30632`)`,
(adapter) => { ... }); update the existing describe block and move its inner
tests unchanged into the new describe.each callback so test names and behavior
remain the same.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 24709e75-36f3-45a6-8926-28306b0c3ee4
📒 Files selected for processing (1)
test/js/sql/sql-pool-lazy-growth.test.ts
There was a problem hiding this comment.
Additional findings (outside current diff — PR may have been updated during review):
-
🟡
test/js/sql/sql.test.ts:87-115— This test never issues a query, so on pre-fix Bun the eager burst (which lived insideconnect(), not the constructor) is never triggered —openedSocketsis 0 on system Bun too. That means the PR description's claim that "all 5 tests fail underUSE_SYSTEM_BUN=1" is inaccurate for this case, and per CLAUDE.md a test that passes withUSE_SYSTEM_BUN=1is not a valid regression test. Consider dropping it (the other 4 tests already cover the fix) or correcting the PR description to say 4 of 5; also note CLAUDE.md says not to usesetTimeoutin tests.Extended reasoning...
What the issue is
The third Postgres test,
"constructing Bun.SQL without running a query opens no connections", asserts that constructing aBun.SQLinstance withmax: 10and never issuing a query results inopenedSockets === 0. The PR description states "Verified all 5 tests fail underUSE_SYSTEM_BUN=1(receive 50 sockets instead of 1)", but this particular test cannot fail on the unpatched runtime: it hasmax: 10(not 50), it never runs a query, and the pre-fix code never opened sockets at construction time. RootCLAUDE.mdline 130 says: "CRITICAL: Verify your test fails withUSE_SYSTEM_BUN=1 bun test <file>and passes withbun bd test <file>. Your test is NOT VALID if it passes withUSE_SYSTEM_BUN=1." This test does not meet that bar.Why pre-fix Bun also opens 0 sockets here
Before this PR, the
PostgresAdapterconstructor only did:this.connections = new Array(connectionInfo.max);
That allocates a sparse array of holes — no
PooledPostgresConnectioninstances are constructed, so no TCP dials happen. The eager burst lived insideconnect(), behindif (!this.poolStarted):this.poolStarted = true; const pollSize = this.connections.length; const firstConnection = new PooledPostgresConnection(this.connectionInfo, this); this.connections[0] = firstConnection; for (let i = 1; i < pollSize; i++) { this.connections[i] = new PooledPostgresConnection(this.connectionInfo, this); }
connect()is only invoked when a query is run. The PR description itself acknowledges this: "The user reports this happens 'immediately' — it's actually on the first use". Since this test never issues a query,connect()is never called andpoolStartedstaysfalse.Step-by-step on system Bun
new SQL({ adapter: "postgres", max: 10, ... })→PostgresAdapterconstructor runs, setsthis.connections = new Array(10)(10 empty holes),poolStarted = false. NoPooledPostgresConnectionis created, so no socket is opened.openedSockets = 0.await new Promise(r => setTimeout(r, 50))→ nothing happens; there is no pending dial to race.openedSocketsis still0.expect(openedSockets).toBe(0)→ passes.await usingdisposal callsclose()→#close().poolStartedisfalse, so the connection-iteration loop is skipped;readyConnections.clear()andwaitingQueue.length = 0are no-ops on empty containers.
So under
USE_SYSTEM_BUN=1this test passes, contradicting the PR description's "all 5 tests fail" claim and violating the CLAUDE.md requirement.Why this is only a nit
This is a test-methodology / PR-description-accuracy issue, not a runtime defect. The other four tests (single query → 1 socket, 20 concurrent on
max: 5→ 5 sockets, ×2 for MySQL) genuinely fail on system Bun and pass with the fix, so the regression is properly covered. One could argue the spirit of the CLAUDE.md rule is "the PR must include at least one test that demonstrably catches the regression", which is satisfied — and this third test is a reasonable forward-looking guard against a future regression where the constructor itself becomes eager. But taken literally, the repo's own contributing guide says a test that passes underUSE_SYSTEM_BUN=1"is NOT VALID", and the PR body's verification claim is factually wrong for this case.Separately, the same test uses
await new Promise(r => setTimeout(r, 50)), which CLAUDE.md line 129 also flags: "Do not usesetTimeoutin tests."Suggested fix
Either:
- Drop this test (tests 1, 2, 4, 5 already cover the fix and satisfy the
USE_SYSTEM_BUN=1rule), or - Keep it as a guard but update the PR description from "all 5 tests fail" to "tests 1, 2, 4, 5 fail under
USE_SYSTEM_BUN=1; test 3 is a guard for already-correct constructor behavior", and replace thesetTimeout(50)with something that doesn't rely on wall-clock time (e.g.await Bun.sleep(0)/ a microtask drain, or just assert immediately since construction is synchronous).
There was a problem hiding this comment.
♻️ Duplicate comments (1)
test/js/sql/sql-pool-lazy-growth.test.ts (1)
35-36: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick winReplace manual adapter loop with
describe.each()Use parameterized suite declaration instead of the
forloop to match the repo’s test style rules.♻️ Proposed change
-for (const adapter of ["postgres", "mysql"] as Adapter[]) { - describe(`${adapter} connection pool grows lazily (`#30632`)`, () => { +describe.each(["postgres", "mysql"] as Adapter[])("%s connection pool grows lazily (`#30632`)", adapter => { test("a single query only opens one TCP connection, not `max`", async () => { using sink = makeSink(); await using sql = new SQL({ @@ await sql`SELECT 1`.catch(() => {}); expect(sink.opened).toBe(1); }); - }); -} + }); +});As per coding guidelines,
test/**/*.test.{ts,js,jsx,tsx,mjs,cjs}should "Usedescribe.each()for parameterized tests".🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@test/js/sql/sql-pool-lazy-growth.test.ts` around lines 35 - 36, Replace the manual for-loop creating per-adapter suites with a parameterized suite using describe.each([...]) to match repo test style: convert the `for (const adapter of ["postgres", "mysql"] as Adapter[]) { describe(\`${adapter} connection pool grows lazily (`#30632`)\`, () => { ... }) }` pattern into `describe.each([ "postgres", "mysql" ])("... %s ...", (adapter) => { ... })`, ensuring you pass the adapter into the inner tests (references: the outer describe block that currently interpolates `${adapter}` and any uses of the `adapter` variable inside tests) so behavior and test names remain identical.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Duplicate comments:
In `@test/js/sql/sql-pool-lazy-growth.test.ts`:
- Around line 35-36: Replace the manual for-loop creating per-adapter suites
with a parameterized suite using describe.each([...]) to match repo test style:
convert the `for (const adapter of ["postgres", "mysql"] as Adapter[]) {
describe(\`${adapter} connection pool grows lazily (`#30632`)\`, () => { ... }) }`
pattern into `describe.each([ "postgres", "mysql" ])("... %s ...", (adapter) =>
{ ... })`, ensuring you pass the adapter into the inner tests (references: the
outer describe block that currently interpolates `${adapter}` and any uses of
the `adapter` variable inside tests) so behavior and test names remain
identical.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 46c2416a-0c8d-4721-8047-ad487409fc60
📒 Files selected for processing (1)
test/js/sql/sql-pool-lazy-growth.test.ts
|
CI red on #54075, but none of the failures touch this diff — the SQL pool change only touches
Already spent my one |
|
@robobun now i wonder is sqlite truly safe from this :) |
|
@RiskyMH sqlite's safe — no pool. |
|
@robobun Skip pool growth when non-retryable auth error is set |
|
Done in d0ade91 — the non-retryable branch now skips |
@robobun What if the password is supplied as a template literal? |
|
A template literal is just a plain string by the time it hits the constructor — The dynamic-creds escape hatch is specifically for the If you want to refresh credentials per retry, wrap it: new Bun.SQL({ password: async () => (await vault.fetch("db-pw")).value }) |
|
@robobun Could there be an edge case scenario where database password is updated and the static password used to connect becomes stale and then the database password is reverted? How can we handle this the with fail fast implementation? |
|
Real concern — let me walk through the lifecycle. Fail-fast only kicks in when every current pool slot has a cached non-retryable auth error. The pool doesn't know the server reverted the password, so cached errors keep getting returned until something clears them. Three things clear them today:
For the specific "static string, rotated, then reverted" flow: pre-fix Bun had the same end-state — it returned the cached auth error too — it just burned an extra TCP+auth RTT per query doing so. The reporter-visible difference is zero; the fix just stops the wasted handshakes. So the recommendation for anyone whose password can change under them is the function shape. If there's demand for a |
|
@robobun Please give me credit in the release notes when this releases. |
|
Release notes are written by the maintainers at release time — I don't control them. You're credited as the reporter in #30632 and the PR body links back, so it's there for whoever drafts the notes. |
|
Second CI run: same story as before — no failures touch this diff. Failed tests are all in fetch/hot-reload/jsc-stress/http2/node-http lanes:
Already used my one |
|
@Lillious reviewed — valid finding. Fixed in bad3076. The growth heuristic was counting all pending connections as capacity for |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/js/internal/sql/mysql.ts`:
- Around line 948-967: The code treats auth failures as non-retryable even when
connectionInfo.password is a function, and currently only calls `#tryGrowPool`()
which returns null at maxPoolSize, causing permanent failure; change the
fallback so that when canDynamicallyAuth (connectionInfo.password is a function)
is true and `#tryGrowPool`() returns null you reuse one of the closed slots
instead of immediately failing — e.g. take a closed slot (the same resource pool
mechanism used when a slot is released), push onConnected into the appropriate
queue (reservedQueue or waitingQueue) and trigger the code path that
opens/initializes a connection for that closed slot (the same path used by
connect()/the slot-reuse logic) so a fresh auth attempt with the dynamic
password is made; keep using storedError/connectionClosedError only when no
dynamic auth is available and no slot can be reused.
In `@src/js/internal/sql/postgres.ts`:
- Around line 1176-1195: The dynamic-credential recovery fails because we only
call `#tryGrowPool`() and add a new slot after checking capacity, which returns
null when connections.length === maxPoolSize and causes callers to replay a
cached auth error; also createConnection() can synchronously call onClose() so
release() may run before we enqueue the waiter. Fix by enqueuing the waiter
(push into reservedQueue or waitingQueue) before attempting any retry/grow, and
when connectionInfo.password is a function attempt to reopen an existing closed
slot (call into the same code path used by `#tryGrowPool`() but targeting a closed
connection) rather than only creating a new slot; ensure the reopen path handles
synchronous onClose() by registering onClose/onConnected callbacks before
invoking PooledPostgresConnection.createConnection() so release()/onClose()
cannot race the enqueue.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 2c3fe454-2b26-422b-932a-51e06bdb9166
📒 Files selected for processing (2)
src/js/internal/sql/mysql.tssrc/js/internal/sql/postgres.ts
|
Third CI run, same pattern — no failures touch the SQL diff:
Already used my one |
|
@robobun Please review coderabbit reported issues |
|
Addressed both findings in 7a274c3: 1. 2. Synchronous Two new tests:
|
|
Fourth CI run, nothing new — still all unrelated to the SQL diff:
Diff is |
There was a problem hiding this comment.
Additional findings (outside current diff — PR may have been updated during review):
-
🟡
test/js/sql/sql-pool-lazy-growth.test.ts:176-178— This test passes5000as the third argument totest(), buttest/CLAUDE.mdsays "CRITICAL: Do not set a timeout on tests. Bun already has timeouts." Drop the explicit5000and rely on Bun's default per-test timeout — the hang you're guarding against will still fail the test.Extended reasoning...
What the guideline says
test/CLAUDE.md(line ~120) is explicit:No timeouts
CRITICAL: Do not set a timeout on tests. Bun already has timeouts.
The repo convention is that the test runner already supplies a per-test timeout, so individual tests should never pass the third
timeoutargument totest().Where the violation is
test/js/sql/sql-pool-lazy-growth.test.ts:145–178:test( "synchronous \`password()\` throw does not hang subsequent queries", async () => { // … }, 5000, // ← explicit per-test timeout );
The in-test comment even says "The
bun:testper-test timeout fails the test if anything hangs, which is the failure mode we're guarding" — that rationale applies equally to Bun's default timeout. Hardcoding5000doesn't add coverage, it just diverges from the repo convention (and is shorter than the default, so it can only make the test more likely to flake under load, not less).Step-by-step
bun:testparses the third positional arg totest()as a millisecond timeout override.- Bun's runner already enforces a default timeout on every test (this is what the CLAUDE.md rule is referring to).
- If the regression this test guards against ever recurs (waiter enqueued after
release()drains the queue → query promise never settles), theawait sql\SELECT ${i}`` on the second iteration never resolves. - With or without the explicit
5000, the runner's timeout fires and the test fails — the explicit value buys nothing functionally.
Why nothing else justifies it
None of the other tests in this file set a timeout, and there's nothing about this case (no Docker, no real DB, the failing path resolves synchronously via
process.nextTick-equivalent paths) that would warrant an exception to the rule.Fix
Remove the trailing
5000argument:test("synchronous \`password()\` throw does not hang subsequent queries", async () => { // … });
This is a style/convention nit, not a functional bug.
|
Fifth CI run, still the same cast of unrelated flakes — none touch the SQL diff:
Diff is |
f32efc8 to
8fd13e5
Compare
|
Sixth CI run — still all unrelated to the SQL diff:
Diff is still just |
|
@robobun This branch is now out of sync with main. Bun was ported to Rust. Merge main into this branch and resolve merge conflicts and re-write in rust. |
|
@robobun Please rebase this for the new rust version of bun |
1 similar comment
|
@robobun Please rebase this for the new rust version of bun |
8fd13e5 to
a93cc67
Compare
|
Status on Diff is Build 62312 is red only on unrelated lanes:
None touch the SQL pool code. Ready for a maintainer to review or merge. |
|
@cirospaciari Could you please review this PR? |
Rebuilt on main (pool plumbing consolidated into BaseSQLAdapter / BasePooledConnection in shared.ts). Previously connect() instantiated every pool slot eagerly on the first query, and each pooled-connection constructor dials TCP immediately, so a single SELECT 1 on a max: 500 pool burst 500 connections. - Pool starts empty; #tryGrowPool() appends one connection at a time (push, so the array is dense — no unassigned holes), capped at maxPoolSize (= options.max). - First connect() opens exactly one connection. - flushConcurrentQueries() grows the pool when queued queries can't be served by ready or pending slots, discounting pending slots already earmarked for reservedQueue. - maxDistribution() divides by the pool ceiling, not the current size. - connect()'s retry scan revives one closed slot per waiter (break after the first), so a query arriving after the pool idled out doesn't redial every slot. - Non-retryable auth failures fail fast with the cached error, unless password is a function (dynamic credential), in which case a closed slot is force-retried so a rotated token can take effect at max: 1. Because the array is dense, the unassigned-hole guards added in #32201 are unnecessary here — a slot being created is pushed only after createPooledConnection returns, so re-entrant scans during a synchronous password() see an empty (not holey) array. Tests: test/js/sql/sql-pool-lazy-growth.test.ts (single query opens 1 socket for both adapters; static-password auth failure fast-fails; function password retries per query at max: 1; synchronous password throw rejects without hanging). Updated sql-close-pending-connection and sql-onconnect-onclose-throw, which asserted the old per-slot eager password/onclose counts at max: 2 — one slot opens per query now.
0ff6392 to
53a46a2
Compare
createPooledConnection() runs a function-valued password() synchronously before the new slot is appended to connections[], so a password() that re-enters the pool while an earlier slot is still mid-handshake read a stale connections.length in #tryGrowPool()'s cap check and kept opening slots past max (unbounded recursion in the worst case). Track slots that are mid-creation in a #growing counter and include it in the cap check so the limit holds under synchronous re-entrancy.
There was a problem hiding this comment.
Latest commit (80d204a) addresses my re-entrancy note with the #growing counter and a regression test, and the bug hunter found nothing new on this revision — but this rewrites the core Bun.SQL pool's connect() / retry / queue-distribution control flow for every postgres/mysql user, so I'd like a maintainer to sign off rather than auto-approving.
Extended reasoning...
Overview
This PR changes Bun.SQL's connection pool from eager (open all max connections on first use) to lazy (grow one slot at a time as demand requires). The substantive change is in src/js/internal/sql/shared.ts (BaseSQLAdapter / BasePooledConnection): a new maxPoolSize field, #tryGrowPool() / #pendingConnectionsCount() helpers, a #growing re-entrancy counter, a forceRetry() escape hatch for function-valued passwords, and a rewrite of connect()'s scan/retry/queue branches plus the flushConcurrentQueries() growth heuristic. Two existing tests are updated for the new one-slot-per-query expectation and a new sql-pool-lazy-growth.test.ts adds five non-Docker regression tests using bare TCP sinks.
Security risks
None identified. No auth, crypto, or permission surfaces are touched; the change governs when sockets are opened, not how credentials are handled. The function-password path already existed and is only re-plumbed.
Level of scrutiny
High. This is production-critical control flow shared by every postgres/mysql/mariadb Bun.SQL user, with subtle interactions between retry classification, reserved vs. waiting queues, idle-timeout revival, and synchronous re-entry from user-supplied password() callbacks. The PR went through roughly ten review rounds, each surfacing a real edge case (auth fast-fail, reserved-queue under-provisioning, max:1 dynamic-credential recovery, dead-slot stranding, post-idle burst, re-entrant growth past max). The current revision looks correct to me and the bug hunter found nothing, but the iteration count itself is evidence that this code path is delicate enough to merit human sign-off.
Other factors
The most recent commit (80d204a) implements the #growing counter I suggested for the re-entrant password() case and adds a dedicated test, so I'm not blocking on anything outstanding. A maintainer review was already requested on the thread; deferring here rather than approving so that request stands.
|
@robobun Main branch was merged into this branch. Please verify there are no broken changes. |
|
@Lillious verified the main merge (`5143f2c4`) locally, no broken changes:
One note so it is not mistaken for a regression: two heavy CI on the merge (build 62607) is green apart from the long-standing flaky |
|
This was tested with my project: https://github.com/Lillious-Networks/Frostfire-Forge and it works. It uses the least number of connections instead of using the max value immediately. Compared before and after and saw the changed expected behavior. Dropped my memory usage down by 80MB |
|
Fair question, it is a real tradeoff.
The per-query latency benefit from pre-opening is real, but it only applies during warm-up, and only if the workload actually reaches that concurrency. Once the pool has grown to its working set the connections are warm and reused, so steady-state acquisition is identical to the eager path, and with If warming the pool is the goal, an explicit pre-warm / minimum-idle option (similar to HikariCP's |
Fixes #30632.
Repro
One query on a
max: 500pool opens all 500 connections. The user reportsthis happens "immediately" — it's actually on the first use, but same net
effect: a pool never scales with load, it jumps straight to the ceiling.
Cause
connect()instantiated every pool slot eagerly on the first call, andeach pooled-connection constructor dials TCP immediately:
Fix
Replace the eager burst with lazy growth. After the rebase onto main this
lands once in
BaseSQLAdapter/BasePooledConnectioninsrc/js/internal/sql/shared.ts(the pool plumbing was consolidated thereby #32145; the original branch patched postgres.ts and mysql.ts
separately).
#tryGrowPool()appends one connection at a time,capped at
maxPoolSize(=options.max).connect()opens exactly one connection.flushConcurrentQueries()grows the pool when queued queries can't beserved by ready or pending slots, discounting pending slots already
earmarked for
reservedQueue.maxDistribution()divides by the pool ceiling rather than the currentsize, so it doesn't collapse every queued query onto the first socket.
connect()'s retry branch grows in parallel while callers wait behindin-flight slots. For non-retryable auth failures it fails fast with the
cached error — unless
passwordis a function (dynamic credential,e.g. IAM token), in which case it grows or force-retries a closed slot
via the new
forceRetry()so a rotated token can take effect even atmax: 1.Verification
Tests
test/js/sql/sql-pool-lazy-growth.test.ts— bareBun.listen()TCPsinks (one inert, one speaking just enough Postgres to reject auth), no
Docker/real DB required:
max(postgres + mysql)max: 1(credentialrotation recovery)
password()throw rejects instead of hanging4 of 5 tests fail on unfixed main; the sync-throw guard passes there
because main now defers
createConnectionfailures viaprocess.nextTick(it documents the contract regardless).