Skip to content

The app engine pool is sized for the run queue, not a lone request - #277

Merged
czpython merged 2 commits into
mainfrom
commonzenpython/eng-863-app-db-engine-pool-is-sqlite-era-default-exhausts-under
Aug 17, 2026
Merged

The app engine pool is sized for the run queue, not a lone request#277
czpython merged 2 commits into
mainfrom
commonzenpython/eng-863-app-db-engine-pool-is-sqlite-era-default-exhausts-under

Conversation

@czpython

@czpython czpython commented Aug 17, 2026

Copy link
Copy Markdown
Owner

A batch of twenty concurrent scout runs exhausted the application database engine. create_engine_from_url set no pool sizing, so it carried SQLAlchemy's defaults — 5 connections plus 10 overflow — while both DBOS engines are explicitly tuned to 20. One application engine backs every concurrent run's steps and all request handling at once; the sixteenth waiter timed out after five seconds and failed its run with QueuePool limit of size 5 overflow 10 reached.

The pool is now explicit: pool_size=20, max_overflow=30 — a modest steady pool with overflow doing the burst work. Overflow connections open on demand and close on return, so the ceiling is high while idle cost is not. Ceiling 50 keeps the appliance, with both DBOS engines at 20, inside Postgres's default 100 connections; steps hold a connection for milliseconds around long agent calls, so 50 simultaneous checkouts implies a fleet far larger than any today. pool_timeout stays at five seconds — a checkout wait blocks the event loop, so the answer to contention is capacity, not longer waits.

Capping the run queue instead was considered and rejected: a parked run still occupies its queue slot while it waits on a human, so a concurrency cap would let a handful of parked gates starve every other run on the appliance. The pool is the safe control point.

This sizing covers the width the queue runs at today; it moves the ceiling rather than abolishing it. The structural end state is the planned async-engine migration, where a connection checkout awaits instead of blocking the loop — pool exhaustion then becomes backpressure and the pool itself is the throttle. Until then the constant is honest: matched to the DBOS engines, with request-handling headroom.

Twenty concurrent scout runs exhausted the application engine: it
carried SQLAlchemy's default QueuePool (5 + 10 overflow) while both
DBOS engines are tuned to twenty, and one engine serves every run's
steps plus request handling. The sixteenth waiter timed out at five
seconds and failed its run.

The pool is now explicit — twenty plus ten overflow — matching the
width the queue actually runs at. pool_timeout stays low: a checkout
wait blocks the event loop, so the fix is capacity, not patience.

The queue itself stays uncapped deliberately: a parked run holds its
queue slot, so a concurrency cap could starve the appliance behind a
handful of runs waiting on humans.
Overflow connections open on demand and close on return, so a high
ceiling costs nothing at idle. Fifty keeps the appliance — with both
DBOS engines at twenty — inside Postgres's default hundred.
@czpython
czpython enabled auto-merge (squash) August 17, 2026 17:52
@czpython
czpython merged commit dc2703c into main Aug 17, 2026
1 check passed
@czpython
czpython deleted the commonzenpython/eng-863-app-db-engine-pool-is-sqlite-era-default-exhausts-under branch August 17, 2026 17:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant