Kyle McChesney
05/28/2026, 8:12 PMMarvin
05/28/2026, 8:12 PMMarvin
05/28/2026, 8:14 PMpython
from prefect import flow, task
class DBPool:
def __init__(self, dsn: str):
self.dsn = dsn
self._pool = None
async def open(self):
# create underlying pool here
self._pool = ...
return self
async def close(self):
# close underlying pool here
...
@task
async def use_db(pool: DBPool, item: int):
# use shared pool here
...
@flow
async def my_flow():
pool = await DBPool("postgresql://...").open()
try:
await use_db.submit(pool, 1)
await use_db.submit(pool, 2)
finally:
await pool.close()
That said, this is only appropriate if the tasks truly share memory. If your tasks are not colocated, do this instead:
python
from prefect import flow, task
@task
async def use_db(dsn: str, item: int):
pool = ...
try:
...
finally:
await pool.close()
@flow
async def my_flow():
await use_db.submit("postgresql://...", 1)
await use_db.submit("postgresql://...", 2)
If your actual goal is controlled access to the DB rather than literal object sharing, Prefect’s concurrency controls are often the better fit than a shared pool. Those let you cap how many tasks hit the DB at once.
Relevant docs:
- Blocks
- Global concurrency limits
- Task runners
So: yes for controlled access, no for a first-class shared resource abstraction.
If you want, I can show the best pattern for your specific DB stack (for example asyncpg, SQLAlchemy, psycopg3, Snowflake, etc.), since the right answer depends a lot on whether your tasks are async and where they run.Kyle McChesney
05/28/2026, 8:25 PMPass it to tasks only if those tasks run in the same process/thread model and the client is safe to share
is there no way to bind it to the flow run context or something so I don't have to add an argument to every single task?Marvin
05/28/2026, 8:26 PMMarvin
05/28/2026, 8:28 PMpython
from contextvars import ContextVar
from prefect import flow, task
from prefect.context import ContextModel
from prefect.task_runners import ThreadPoolTaskRunner
class DBContext(ContextModel):
__var__ = ContextVar("db_context")
pool: object
@task
def task_a():
pool = DBContext.get().pool
# use pool here
...
@task
def task_b():
pool = DBContext.get().pool
# use pool here
...
@flow(task_runner=ThreadPoolTaskRunner())
def my_flow():
pool = create_pool()
try:
with DBContext(pool=pool):
task_a.submit()
task_b.submit()
finally:
close_pool(pool)
Why I’d be cautious recommending this:
- it couples your tasks to ambient runtime state
- it only works for same-process runners like ThreadPoolTaskRunner
- it will not work reliably if tasks run in another process / worker / container
- it’s less explicit, so testing and reuse get harder
So if you want ergonomics without passing the pool into every task, there are two better patterns depending on your setup:
1. Use a helper function instead of task args
Tasks call a shared accessor that reads from custom context when available.
python
def get_pool():
ctx = DBContext.get()
if ctx is None:
raise RuntimeError("No DBContext is active")
return ctx.pool
@task
def task_a():
pool = get_pool()
...
That cleans up signatures, but still has the same same-process limitation.
2. Pass config, not the pool
If your tasks may ever leave the process boundary, pass a DSN / block / settings object and lazily create connections in task code. That’s the most portable pattern.
Also relevant if your main concern is controlled usage rather than literal sharing: use concurrency limits to cap simultaneous DB access:
- Global concurrency limits
Useful references:
- Task runners
- Blocks
So: yes, with custom ContextModel, but only as a local same-process convenience hack/pattern — not as a universal Prefect resource binding feature.
If you want, I can show you a clean production-safe pattern for:
- same-process shared SQLAlchemy engine/sessionmaker, or
- distributed-safe per-task DB access with minimal boilerplate