Chalama Reddy
02/18/2026, 12:16 PMMarvin
02/18/2026, 12:16 PMMarvin
02/18/2026, 12:22 PM--workers or multiple replicas/pods), total possible connections = processes × (pool_size + max_overflow).
Why you’re likely seeing it with a 100-connection cap
- With the defaults, 7 API processes can consume up to 7 × 15 = 105 connections, not counting other apps or admin connections.
- Background API tasks (cleanup, scheduling, leases, etc.) also borrow connections from the same pool and can hold them under load.
What to check first
1) How many API processes are you running?
- If you run prefect server start --workers N, total connections scale linearly with N. In 3.6.7 there is a --workers flag; default is 1.
- If you’re in Kubernetes or Compose, check if you’ve scaled the API deployment to multiple replicas.
2) Whether you’ve already tuned the pool via env vars (see below).
3) Whether you’re running PgBouncer in front of Postgres.
How to cap/shape the pool to fit under 100
- Start by clamping max overflow and increasing timeout, so bursts queue instead of failing:
# Cap each API process at 5 connections and wait longer before erroring
export PREFECT_SERVER_DATABASE_SQLALCHEMY_POOL_SIZE=5
export PREFECT_SERVER_DATABASE_SQLALCHEMY_MAX_OVERFLOW=0
export PREFECT_SERVER_DATABASE_SQLALCHEMY_POOL_TIMEOUT=60
export PREFECT_SERVER_DATABASE_SQLALCHEMY_POOL_RECYCLE=3600
- If you have multiple API processes/replicas, multiply pool_size across them and ensure the sum stays comfortably below 100 (account for other DB clients too). For example, with 4 API processes and a 100-connection DB cap, a safe starting point is pool_size=5, max_overflow=0 → 4 × 5 = 20 total.
- If you need higher concurrency from the API layer, either:
- Increase Postgres max_connections above 100, or
- Put PgBouncer in front of Postgres and point Prefect at PgBouncer (transaction pooling). If you do this, disable asyncpg’s statement cache:
# Point Prefect at PgBouncer and disable statement cache for transaction pooling
export PREFECT_SERVER_DATABASE_CONNECTION_URL="<postgresql+asyncpg://user:pass@pgbouncer:6432/prefect>"
export PREFECT_SERVER_DATABASE_SQLALCHEMY_CONNECT_ARGS_STATEMENT_CACHE_SIZE=0
Notes on workers
- prefect server start --workers N controls uvicorn processes for the API/UI. Each process has its own pool, so N increases DB connections linearly. There’s no env var equivalent for --workers.
- In multi-worker mode (--workers > 1), services are not run in-process and Redis is required. If you don’t need multiple API processes, stick with 1–2.
How to verify/tune
- See your effective settings:
prefect config view | grep -i server.database
- Monitor connections in Postgres:
SELECT datname, count(*) FROM pg_stat_activity GROUP BY datname;
SELECT application_name, count(*) FROM pg_stat_activity WHERE datname='prefect' GROUP BY application_name;
- For deeper debugging:
export PREFECT_SERVER_DATABASE_ECHO=1
export PREFECT_SERVER_LOGGING_LEVEL=DEBUG
Source references
- Database configuration (pool settings and connect args): Database configuration source
- Server CLI (workers flag): Server CLI sourceMarvin
02/18/2026, 12:22 PM--workers N, or number of replicas),
- Whether PgBouncer is in use,
- Any pool env vars you’ve already set,
I can help you pick concrete pool values that keep you safely under your 100-connection limit.Chalama Reddy
02/18/2026, 12:41 PMChalama Reddy
02/18/2026, 12:43 PMMarvin
02/18/2026, 12:43 PMMarvin
02/18/2026, 12:45 PMprefect server start --workers N or your container’s command/args). If each of your 4 API servers uses, say, 4 uvicorn workers, that’s 16 processes total, and with defaults you could hit 16 × 15 = 240 connections.
2) Clamp the SQLAlchemy pool per process so total stays safely below 100. A good, safe starting point:
- If you have 4 API servers and 1 uvicorn worker each:
- Set pool_size=8, max_overflow=0 → 4 × 8 = 32 total DB connections
- If you have 4 API servers and 4 uvicorn workers each (16 processes):
- Set pool_size=5, max_overflow=0 → 16 × 5 = 80 total DB connections
Recommended env vars (apply to each API server/pod):
# Cap connections per API process
export PREFECT_SERVER_DATABASE_SQLALCHEMY_POOL_SIZE=5
export PREFECT_SERVER_DATABASE_SQLALCHEMY_MAX_OVERFLOW=0
# Make bursts wait instead of failing fast
export PREFECT_SERVER_DATABASE_SQLALCHEMY_POOL_TIMEOUT=60
# Reasonable recycle to avoid stale connections
export PREFECT_SERVER_DATABASE_SQLALCHEMY_POOL_RECYCLE=3600
# Optional: make connections easy to identify in pg_stat_activity
export PREFECT_SERVER_DATABASE_SQLALCHEMY_CONNECT_ARGS_APPLICATION_NAME=prefect-api
If you’re using PgBouncer, point the API to PgBouncer and disable asyncpg’s statement cache for transaction pooling:
export PREFECT_SERVER_DATABASE_CONNECTION_URL="<postgresql+asyncpg://USER:PASS@pgbouncer:6432/prefect>"
export PREFECT_SERVER_DATABASE_SQLALCHEMY_CONNECT_ARGS_STATEMENT_CACHE_SIZE=0
Also verify/adjust the number of uvicorn workers:
- CLI: prefect server start --workers N (default is 1)
- If you are running multiple API replicas, multiply by replicas
Source: Server CLI source
Where these pool settings come from:
- Database configuration (SQLAlchemy pool)
How to confirm what’s active:
# See effective settings on a running API server
prefect config view | grep -i server.database
# In Postgres
SELECT application_name, count(*)
FROM pg_stat_activity
WHERE datname = 'prefect'
GROUP BY application_name;
A couple of quick clarifying questions so I can give you exact numbers:
- How many uvicorn workers are running per API server (per replica/pod)?
- Are you using PgBouncer?
- Have you already set any of the PREFECT_SERVER_DATABASE_SQLALCHEMY_* env vars?
Once I have that, I’ll calculate precise pool_size/max_overflow values that keep you well under the 100-connection limit with headroom.