jusstol
07/10/2026, 8:55 AMMarvin
07/10/2026, 8:55 AMMarvin
07/10/2026, 8:56 AMPREFECT_SERVER_EVENTS_RETENTION_PERIOD.
Example: keep only 2 days of events
bash
export PREFECT_SERVER_EVENTS_RETENTION_PERIOD="2d"
You can also use seconds:
bash
export PREFECT_SERVER_EVENTS_RETENTION_PERIOD="172800"
### 2) Make sure DB vacuum cleanup is enabled
The cleanup runs through the DB vacuum service.
bash
export PREFECT_SERVER_SERVICES_DB_VACUUM_ENABLED="events"
export PREFECT_SERVER_SERVICES_DB_VACUUM_LOOP_SECONDS="3600"
export PREFECT_SERVER_SERVICES_DB_VACUUM_BATCH_SIZE="200"
That means:
- cleanup runs every hour
- deletes old events in batches
- removes from both events and event_resources
### 3) Restart Prefect server/services
After changing env vars, restart your Prefect server so the settings take effect.
### 4) Check what’s in the DB
Useful SQL:
sql
SELECT count(*), min(occurred), max(occurred)
FROM events;
SELECT count(*), min(occurred), max(occurred)
FROM event_resources;
To see how many old rows would be removed:
sql
SELECT count(*)
FROM events
WHERE occurred < now() - interval '2 days';
### 5) If you need to manually purge old events once
Delete event_resources first, then events.
sql
DELETE FROM event_resources
WHERE occurred < now() - interval '2 days';
DELETE FROM events
WHERE occurred < now() - interval '2 days';
Afterward, reclaim space:
sql
VACUUM ANALYZE event_resources;
VACUUM ANALYZE events;
### 6) Safer manual cleanup for large tables
Since your DB is fairly large, batch deletes are safer than one huge delete:
sql
DELETE FROM event_resources
WHERE id IN (
SELECT id
FROM event_resources
WHERE occurred < now() - interval '2 days'
LIMIT 10000
);
DELETE FROM events
WHERE id IN (
SELECT id
FROM events
WHERE occurred < now() - interval '2 days'
LIMIT 10000
);
Run those repeatedly until 0 rows are deleted, then:
sql
VACUUM ANALYZE event_resources;
VACUUM ANALYZE events;
### Important notes
- Retention is based on the occurred timestamp.
- There is no separate Prefect CLI command to purge events manually.
- Prefect’s built-in vacuum deletes old event_resources and events automatically.
- If disk space does not shrink after deletes, that’s a PostgreSQL vacuum/reclaim issue, not a Prefect issue.
### Docs
See Prefect’s database maintenance docs:
Database maintenance
If you want, I can also give you:
- a one-time SQL cleanup script for events
- or a full retention plan for events, log, and flow run tables based on your 28GB DB.jusstol
07/10/2026, 9:48 AMjusstol
07/10/2026, 9:49 AMMarvin
07/10/2026, 9:49 AMMarvin
07/10/2026, 9:50 AMflow_runs.
By default, Prefect mainly cleans events automatically. Flow run cleanup must be explicitly enabled.
## Recommended setup
bash
export PREFECT_SERVER_SERVICES_DB_VACUUM_ENABLED="events,flow_runs"
export PREFECT_SERVER_SERVICES_DB_VACUUM_RETENTION_PERIOD="2592000" # 30 days
export PREFECT_EVENTS_RETENTION_PERIOD="604800" # 7 days
export PREFECT_SERVER_SERVICES_DB_VACUUM_LOOP_SECONDS="3600"
export PREFECT_SERVER_SERVICES_DB_VACUUM_BATCH_SIZE="200"
This will make Prefect periodically clean up:
- old terminal flow runs
- orphaned logs
- orphaned artifacts
- stale artifact collections
- old events
## What flow runs are eligible
Cleanup is based on:
- end_time older than retention period
- terminal states only:
- COMPLETED
- FAILED
- CANCELLED
- CRASHED
So active/running flow runs are not touched.
## Important detail
Flow run retention uses:
PREFECT_SERVER_SERVICES_DB_VACUUM_RETENTION_PERIOD
Default is 90 days.
## Check what would be deleted
sql
SELECT count(*) AS flow_runs_to_delete
FROM flow_run
WHERE end_time < now() - interval '30 days'
AND state_type IN ('COMPLETED', 'FAILED', 'CANCELLED', 'CRASHED');
You can also inspect oldest runs:
sql
SELECT id, name, state_type, end_time
FROM flow_run
WHERE end_time < now() - interval '30 days'
AND state_type IN ('COMPLETED', 'FAILED', 'CANCELLED', 'CRASHED')
ORDER BY end_time
LIMIT 20;
## One-time manual cleanup
If you want to manually delete old flow runs, Prefect does support deleting flow runs through the API/client, which is safer than raw SQL because it respects app-level cleanup behavior.
If you need SQL, start with a preview and backup first.
sql
DELETE FROM flow_run
WHERE end_time < now() - interval '30 days'
AND state_type IN ('COMPLETED', 'FAILED', 'CANCELLED', 'CRASHED');
## Caution on direct SQL
Direct SQL may leave related rows/orphans depending on table relationships and how much historical data you remove at once. Since your database is already fairly large, I would strongly prefer enabling built-in vacuum over hand-deleting flow_run rows.
If you do manual deletion:
- do it in batches
- run VACUUM ANALYZE after
- keep the Prefect vacuum service enabled so orphan cleanup can catch leftovers
Example batch delete:
sql
DELETE FROM flow_run
WHERE id IN (
SELECT id
FROM flow_run
WHERE end_time < now() - interval '30 days'
AND state_type IN ('COMPLETED', 'FAILED', 'CANCELLED', 'CRASHED')
LIMIT 1000
);
Then:
sql
VACUUM ANALYZE flow_run;
## Practical recommendation for your DB
Given your 28GB self-hosted Postgres setup, I’d suggest:
- `events`: 1–3 days
- `flow_runs`: 14–30 days
- enable events,flow_runs vacuum immediately
## Docs
Prefect database maintenance:
Database maintenance
If you want, I can next give you a complete retention recipe for:
- events
- flow_run
- log
- flow_run_state
with exact env vars and SQL checks for each.jusstol
07/10/2026, 9:58 AMFile "/usr/local/lib/python3.11/site-packages/sqlalchemy/dialects/postgresql/asyncpg.py", line 558, in _prepare_and_execut
self._handle_exception(error)
File "/usr/local/lib/python3.11/site-packages/sqlalchemy/dialects/postgresql/asyncpg.py", line 508, in _handle_exception
self._adapt_connection._handle_exception(error)
File "/usr/local/lib/python3.11/site-packages/sqlalchemy/dialects/postgresql/asyncpg.py", line 794, in _handle_exception
raise error
File "/usr/local/lib/python3.11/site-packages/sqlalchemy/dialects/postgresql/asyncpg.py", line 545, in _prepare_and_execut
self._rows = deque(await prepared_stmt.fetch(*parameters))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/site-packages/asyncpg/prepared_stmt.py", line 176, in fetch
data = await self.__bind_execute(args, 0, timeout)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/site-packages/asyncpg/prepared_stmt.py", line 267, in __bind_execute
data, status, _ = await self.__do_execute(
^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/site-packages/asyncpg/prepared_stmt.py", line 256, in __do_execute
return await executor(protocol)
^^^^^^^^^^^^^^^^^^^^^^^^
File "asyncpg/protocol/protocol.pyx", line 206, in bind_execute
TimeoutError
but i have no problem connecting to prefects postgresql database manually. how can i debug this ?Marvin
07/10/2026, 9:58 AMMarvin
07/10/2026, 10:00 AMasyncpg ... TimeoutError during bind_execute is consistent with that.
## Why deleting a flow run can be slow
When Prefect deletes a flow run, it is not just removing one row.
Deleting a flow_run can cascade into related records like:
- flow_run_state
- task_run
- task_run_state
- flow_run_input
Also, logs/artifacts may involve additional cleanup behavior.
So if that run has lots of task runs / state history, deletion may take longer than Prefect's DB statement timeout.
## First thing to check: Prefect DB timeout
Prefect server uses a database timeout setting. In practice, a common default is around 10 seconds.
Try increasing:
bash
export PREFECT_SERVER_DATABASE_TIMEOUT=60
Then restart your Prefect server.
If you're using PREFECT_API_DATABASE_TIMEOUT in your setup, check that too, but for a server deployment I'd start with:
bash
export PREFECT_SERVER_DATABASE_TIMEOUT=60
You may also want:
bash
export PREFECT_SERVER_DATABASE_CONNECTION_TIMEOUT=10
export PREFECT_SERVER_DATABASE_SQLALCHEMY_POOL_TIMEOUT=60
export PREFECT_SERVER_DATABASE_SQLALCHEMY_POOL_SIZE=10
export PREFECT_SERVER_DATABASE_SQLALCHEMY_MAX_OVERFLOW=20
## How to debug it properly
### 1) Find out how big that flow run is
Run this against Postgres for the problematic flow run ID:
sql
SELECT COUNT(*) AS task_runs
FROM task_run
WHERE flow_run_id = '<FLOW_RUN_ID>';
SELECT COUNT(*) AS flow_run_states
FROM flow_run_state
WHERE flow_run_id = '<FLOW_RUN_ID>';
SELECT COUNT(*) AS task_run_states
FROM task_run_state
WHERE task_run_id IN (
SELECT id
FROM task_run
WHERE flow_run_id = '<FLOW_RUN_ID>'
);
If those counts are large, that strongly supports the timeout diagnosis.
### 2) Watch active Postgres queries while you click delete
In Postgres:
sql
SELECT pid, now() - query_start AS runtime, state, wait_event_type, wait_event, query
FROM pg_stat_activity
WHERE state <> 'idle'
ORDER BY query_start;
If you see a long-running DELETE against Prefect tables, that’s your culprit.
### 3) Check for locks
Sometimes the delete is blocked, not just slow.
sql
SELECT blocked.pid AS blocked_pid,
blocked.query AS blocked_query,
blocking.pid AS blocking_pid,
blocking.query AS blocking_query
FROM pg_stat_activity blocked
JOIN pg_locks blocked_locks ON blocked.pid = blocked_locks.pid
JOIN pg_locks blocking_locks
ON blocked_locks.locktype = blocking_locks.locktype
AND blocked_locks.database IS NOT DISTINCT FROM blocking_locks.database
AND blocked_locks.relation IS NOT DISTINCT FROM blocking_locks.relation
AND blocked_locks.page IS NOT DISTINCT FROM blocking_locks.page
AND blocked_locks.tuple IS NOT DISTINCT FROM blocking_locks.tuple
AND blocked_locks.virtualxid IS NOT DISTINCT FROM blocking_locks.virtualxid
AND blocked_locks.transactionid IS NOT DISTINCT FROM blocking_locks.transactionid
AND blocked_locks.classid IS NOT DISTINCT FROM blocking_locks.classid
AND blocked_locks.objid IS NOT DISTINCT FROM blocking_locks.objid
AND blocked_locks.objsubid IS NOT DISTINCT FROM blocking_locks.objsubid
AND blocked_locks.pid <> blocking_locks.pid
JOIN pg_stat_activity blocking ON blocking.pid = blocking_locks.pid
WHERE NOT blocked_locks.granted;
### 4) Check table sizes
Given your DB size, the biggest related tables matter.
sql
SELECT
relname,
n_live_tup,
n_dead_tup,
pg_size_pretty(pg_total_relation_size(relid)) AS total_size
FROM pg_stat_user_tables
WHERE relname IN ('flow_run', 'flow_run_state', 'task_run', 'task_run_state', 'log', 'events', 'event_resources')
ORDER BY pg_total_relation_size(relid) DESC;Marvin
07/10/2026, 10:00 AMbash
export PREFECT_SERVER_DATABASE_ECHO=true
Then restart the server and retry. This can help confirm which statement is timing out.
## Likely fixes
### Option A: Increase timeout
Fastest test:
bash
export PREFECT_SERVER_DATABASE_TIMEOUT=60
If needed, try 120.
### Option B: Clean up old data first
If your related tables are bloated, deleting one run can become expensive. Since you're self-hosted and already looking at retention, enabling automated cleanup can help overall DB performance:
bash
export PREFECT_SERVER_SERVICES_DB_VACUUM_ENABLED="events,flow_runs"
export PREFECT_SERVER_SERVICES_DB_VACUUM_RETENTION_PERIOD="2592000"
export PREFECT_EVENTS_RETENTION_PERIOD="172800"
### Option C: Delete problematic runs during low activity
If locks/contention are involved, trying during a quiet window can help.
### Option D: Manual deletion only if necessary
If the UI/API keeps timing out, you can try deleting directly in SQL, but I’d treat that as a careful maintenance operation, because it bypasses Prefect’s application-layer behavior.
If you go that route, back up first and inspect dependencies carefully.
## My strongest guess
Given your environment and the error:
- not a basic DB connectivity issue
- most likely a large or blocked cascading delete
- first remediation to try is increase PREFECT_SERVER_DATABASE_TIMEOUT to 60s and inspect row counts for that specific run
## Useful docs
Database maintenance:
Database maintenance
If you want, I can help you with a step-by-step triage plan next:
1. get the flow run ID
2. run the exact SQL checks
3. interpret the results
4. decide whether to raise timeout, vacuum, or do manual cleanup