Liliya Angelova
03/25/2026, 1:19 PMMarvin
03/25/2026, 1:19 PMMarvin
03/25/2026, 1:25 PMtask_run.flow_run_id should cascade on delete.
3) Log cleanup is asynchronous (or may be disabled)
- The API enqueues per-run log deletion tasks. If your API server wasn’t running, or if those docket tasks failed, you’ll still see old log rows.
- Additionally, logs that are not associated with a flow_run_id (rare, but possible) are not removed by flow-run deletion and require the vacuum service or a custom cleanup.
4) Subflows are special
- Deleting a parent’s task run does not delete child flow runs. Those child runs (and their task runs/logs) will persist until you explicitly delete them based on your policy.
How to diagnose quickly
1) Pick one old task_run row and check its flow run:
-- Find the parent flow run
SELECT flow_run_id FROM task_run WHERE id = '<task_run_id>';
-- See if that flow run still exists and why it wasn’t deleted
SELECT id, state_type, start_time, end_time, parent_task_run_id
FROM flow_run
WHERE id = '<flow_run_id>';
- If the flow_run still exists: your filter likely excluded it (e.g., non-terminal, start_time within 30 days, or subflow).
- If the flow_run does not exist: your DB likely lacks the ON DELETE CASCADE on task_run.flow_run_id (see next step).
2) Verify foreign key cascade on task_run.flow_run_id (Postgres):
SELECT
tc.constraint_name,
rc.delete_rule
FROM information_schema.table_constraints AS tc
JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name
JOIN information_schema.referential_constraints AS rc ON tc.constraint_name = rc.constraint_name
WHERE
tc.constraint_type = 'FOREIGN KEY'
AND tc.table_name = 'task_run'
AND kcu.column_name = 'flow_run_id';
- Expected: delete_rule = CASCADE. If not, you’ll need to upgrade/apply migrations so the schema matches Prefect 3.x.
3) Confirm log deletion ran
- Check your API server logs for entries related to “delete_flow_run_logs” after your retention flow. If the server wasn’t running, the log cleanup wouldn’t have executed.
- Optional: enable the built-in database vacuum service to catch orphaned logs/artifacts over time:
- PREFECT_SERVER_SERVICES_DB_VACUUM_ENABLED=flow_runs,events
- PREFECT_SERVER_SERVICES_DB_VACUUM_RETENTION_PERIOD=... (for flow runs)
See: Database maintenance
References
- Data retention guide: Database maintenance - Data retentionMarvin
03/25/2026, 1:25 PMLiliya Angelova
03/25/2026, 1:33 PM2026-03-25T14:30:24.619+02:00 result = conn.execute(
2026-03-25T14:30:24.619+02:00 ^^^^^^^^^^^^^
2026-03-25T14:30:24.619+02:00 File "/usr/local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1419, in execute
2026-03-25T14:30:24.619+02:00 return meth(
2026-03-25T14:30:24.619+02:00 ^^^^^
2026-03-25T14:30:24.619+02:00 File "/usr/local/lib/python3.11/site-packages/sqlalchemy/sql/elements.py", line 527, in _execute_on_connection
2026-03-25T14:30:24.619+02:00 return connection._execute_clauseelement(
2026-03-25T14:30:24.619+02:00 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2026-03-25T14:30:24.619+02:00 File "/usr/local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1641, in _execute_clauseelement
2026-03-25T14:30:24.619+02:00 ret = self._execute_context(
2026-03-25T14:30:24.619+02:00 ^^^^^^^^^^^^^^^^^^^^^^
2026-03-25T14:30:24.619+02:00 File "/usr/local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1846, in _execute_context
2026-03-25T14:30:24.619+02:00 return self._exec_single_context(
2026-03-25T14:30:24.619+02:00 ^^^^^^^^^^^^^^^^^^^^^^^^^^
2026-03-25T14:30:24.619+02:00 File "/usr/local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1986, in _exec_single_context
2026-03-25T14:30:24.619+02:00 self._handle_dbapi_exception(
2026-03-25T14:30:24.619+02:00 File "/usr/local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 2366, in _handle_dbapi_exception
2026-03-25T14:30:24.619+02:00 raise exc_info[1].with_traceback(exc_info[2])
2026-03-25T14:30:24.619+02:00 File "/usr/local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1967, in _exec_single_context
2026-03-25T14:30:24.619+02:00 self.dialect.do_execute(
2026-03-25T14:30:24.619+02:00 File "/usr/local/lib/python3.11/site-packages/sqlalchemy/engine/default.py", line 952, in do_execute
2026-03-25T14:30:24.619+02:00 cursor.execute(statement, parameters)
2026-03-25T14:30:24.619+02:00 File "/usr/local/lib/python3.11/site-packages/sqlalchemy/dialects/postgresql/asyncpg.py", line 585, in execute
2026-03-25T14:30:24.619+02:00 self._adapt_connection.await_(
2026-03-25T14:30:24.619+02:00 File "/usr/local/lib/python3.11/site-packages/sqlalchemy/util/_concurrency_py3k.py", line 132, in await_only
2026-03-25T14:30:24.619+02:00 return current.parent.switch(awaitable) # type: ignore[no-any-return,attr-defined] # noqa: E501
2026-03-25T14:30:24.619+02:00 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2026-03-25T14:30:24.619+02:00 File "/usr/local/lib/python3.11/site-packages/sqlalchemy/util/_concurrency_py3k.py", line 196, in greenlet_spawn
2026-03-25T14:30:24.619+02:00 value = await result
2026-03-25T14:30:24.619+02:00 ^^^^^^^^^^^^
2026-03-25T14:30:24.619+02:00 File "/usr/local/lib/python3.11/site-packages/sqlalchemy/dialects/postgresql/asyncpg.py", line 563, in _prepare_and_execute
2026-03-25T14:30:24.619+02:00 self._handle_exception(error)
2026-03-25T14:30:24.619+02:00 File "/usr/local/lib/python3.11/site-packages/sqlalchemy/dialects/postgresql/asyncpg.py", line 513, in _handle_exception
2026-03-25T14:30:24.619+02:00 self._adapt_connection._handle_exception(error)
2026-03-25T14:30:24.619+02:00 File "/usr/local/lib/python3.11/site-packages/sqlalchemy/dialects/postgresql/asyncpg.py", line 799, in _handle_exception
2026-03-25T14:30:24.619+02:00 raise error
2026-03-25T14:30:24.619+02:00 File "/usr/local/lib/python3.11/site-packages/sqlalchemy/dialects/postgresql/asyncpg.py", line 550, in _prepare_and_execute
2026-03-25T14:30:24.619+02:00 self._rows = deque(await prepared_stmt.fetch(*parameters))
2026-03-25T14:30:24.619+02:00 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2026-03-25T14:30:24.619+02:00 File "/usr/local/lib/python3.11/site-packages/asyncpg/prepared_stmt.py", line 177, in fetch
2026-03-25T14:30:24.619+02:00 data = await self.__bind_execute(args, 0, timeout)
2026-03-25T14:30:24.619+02:00 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2026-03-25T14:30:24.619+02:00 File "/usr/local/lib/python3.11/site-packages/asyncpg/prepared_stmt.py", line 268, in __bind_execute
2026-03-25T14:30:24.619+02:00 data, status, _ = await self.__do_execute(
2026-03-25T14:30:24.619+02:00 ^^^^^^^^^^^^^^^^^^^^^^^^
2026-03-25T14:30:24.619+02:00 File "/usr/local/lib/python3.11/site-packages/asyncpg/prepared_stmt.py", line 257, in __do_execute
2026-03-25T14:30:24.619+02:00 return await executor(protocol)
2026-03-25T14:30:24.619+02:00 ^^^^^^^^^^^^^^^^^^^^^^^^
2026-03-25T14:30:24.619+02:00 File "asyncpg/protocol/protocol.pyx", line 205, in bind_execute
2026-03-25T14:30:24.619+02:00TimeoutErrorMarvin
03/25/2026, 1:34 PMMarvin
03/25/2026, 1:34 PMLiliya Angelova
03/25/2026, 1:37 PMresult: Result[Any] = compile_state_cls.orm_execute_statement(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/site-packages/sqlalchemy/orm/bulk_persistence.py", line 2033, in orm_execute_statement
return super().orm_execute_statement(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/site-packages/sqlalchemy/orm/context.py", line 306, in orm_execute_statement
result = conn.execute(
^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1419, in execute
return meth(
^^^^^
File "/usr/local/lib/python3.11/site-packages/sqlalchemy/sql/elements.py", line 527, in _execute_on_connection
return connection._execute_clauseelement(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1641, in _execute_clauseelement
ret = self._execute_context(
^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1846, in _execute_context
return self._exec_single_context(
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1986, in _exec_single_context
self._handle_dbapi_exception(
File "/usr/local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 2366, in _handle_dbapi_exception
raise exc_info[1].with_traceback(exc_info[2])
File "/usr/local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1967, in _exec_single_context
self.dialect.do_execute(
File "/usr/local/lib/python3.11/site-packages/sqlalchemy/engine/default.py", line 952, in do_execute
cursor.execute(statement, parameters)
File "/usr/local/lib/python3.11/site-packages/sqlalchemy/dialects/postgresql/asyncpg.py", line 585, in execute
self._adapt_connection.await_(
File "/usr/local/lib/python3.11/site-packages/sqlalchemy/util/_concurrency_py3k.py", line 132, in await_only
return current.parent.switch(awaitable) # type: ignore[no-any-return,attr-defined] # noqa: E501
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/site-packages/sqlalchemy/util/_concurrency_py3k.py", line 196, in greenlet_spawn
value = await result
^^^^^^^^^^^^
File "/usr/local/lib/python3.11/site-packages/sqlalchemy/dialects/postgresql/asyncpg.py", line 563, in _prepare_and_execute
self._handle_exception(error)
File "/usr/local/lib/python3.11/site-packages/sqlalchemy/dialects/postgresql/asyncpg.py", line 513, in _handle_exception
self._adapt_connection._handle_exception(error)
File "/usr/local/lib/python3.11/site-packages/sqlalchemy/dialects/postgresql/asyncpg.py", line 799, in _handle_exception
raise error
File "/usr/local/lib/python3.11/site-packages/sqlalchemy/dialects/postgresql/asyncpg.py", line 550, in _prepare_and_execute
self._rows = deque(await prepared_stmt.fetch(*parameters))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/site-packages/asyncpg/prepared_stmt.py", line 177, in fetch
data = await self.__bind_execute(args, 0, timeout)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/site-packages/asyncpg/prepared_stmt.py", line 268, in __bind_execute
data, status, _ = await self.__do_execute(
^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/site-packages/asyncpg/prepared_stmt.py", line 257, in __do_execute
return await executor(protocol)
^^^^^^^^^^^^^^^^^^^^^^^^
File "asyncpg/protocol/protocol.pyx", line 205, in bind_execute
TimeoutErrorMarvin
03/25/2026, 1:37 PMMarvin
03/25/2026, 1:40 PM# e.g., bump statement timeout to 5 minutes and connection timeout to 30s
export PREFECT_API_DATABASE_TIMEOUT=300
export PREFECT_API_DATABASE_CONNECTION_TIMEOUT=30
# then restart your Prefect server/API
Docs: Settings reference
- Ensure you’re deleting in batches (the docs flow does this). For very large datasets, use smaller batches and brief sleeps between them.
- Check for locks or slow plans:
- Look at Postgres logs for statement_timeout or blocking locks
- Run EXPLAIN ANALYZE on the slow query (if you have it)
- Verify expected indexes exist (Prefect migrations create indexes on columns like flow_run.start_time/state_type/parent_task_run_id, task_run.flow_run_id, logs.flow_run_id/timestamp). If your schema is old, upgrade migrations:
PREFECT_API_DATABASE_TIMEOUT=600 prefect server database upgrade -y
- If this happened during log cleanup after deletions: that delete can touch a lot of rows. Increasing PREFECT_API_DATABASE_TIMEOUT usually resolves it; you can also enable the built-in vacuum service to clean orphans in the background:
Database maintenance
If you can share when this fired (e.g., during your retention flow’s read vs. delete step) and your DB size, I can suggest tighter, query-specific tuning.