Pav Staz
02/26/2026, 5:52 PMMarvin
02/26/2026, 5:52 PMNate
02/26/2026, 5:54 PMPav Staz
02/26/2026, 5:54 PMNate
02/26/2026, 5:55 PMNate
02/26/2026, 5:55 PMMarvin
02/26/2026, 5:58 PMprefect config set PREFECT_SERVER_SERVICES_DB_VACUUM_ENABLED=true
prefect config set PREFECT_SERVER_SERVICES_DB_VACUUM_LOOP_SECONDS=604800
# optional: change retention (default is 90 days)
prefect config set PREFECT_SERVER_SERVICES_DB_VACUUM_RETENTION_PERIOD=7776000
# optional: batch size per transaction (default 200)
prefect config set PREFECT_SERVER_SERVICES_DB_VACUUM_BATCH_SIZE=200
- Then run your Prefect Server as usual. The vacuum will run in the background on that cadence.
Notes
- Runs at the server level; there’s no separate CLI to “manually trigger” it.
- Tune LOOP_SECONDS (frequency), RETENTION_PERIOD (in seconds), and BATCH_SIZE to your needs.
- Test in non-prod and back up your DB before enabling—this deletes data.
If you specifically want to use flow.serve to schedule weekly
You don’t need .serve for the built-in vacuum (it’s a server service), but if you want a visible, deployment-style weekly job (e.g., to delete old runs), you can implement a small retention flow and schedule it weekly with .serve:
from datetime import datetime, timedelta, timezone
import asyncio
from prefect import flow, task, get_run_logger
from prefect.client.orchestration import get_client
from prefect.client.schemas import StateType
from prefect.client.schemas.filters import (
FlowRunFilter,
FlowRunFilterStartTime,
FlowRunFilterState,
FlowRunFilterStateType,
FlowRunFilterParentFlowRunId,
)
@task
async def delete_old_top_level_flow_runs(days_to_keep: int = 90, batch_size: int = 200):
logger = get_run_logger()
cutoff = datetime.now(timezone.utc) - timedelta(days=days_to_keep)
async with get_client() as client:
flow_run_filter = FlowRunFilter(
start_time=FlowRunFilterStartTime(before_=cutoff),
state=FlowRunFilterState(
type=FlowRunFilterStateType(
any_=[StateType.COMPLETED, StateType.FAILED, StateType.CANCELLED]
)
),
parent_flow_run_id=FlowRunFilterParentFlowRunId(is_null_=True),
)
total_deleted = 0
while True:
runs = await client.read_flow_runs(flow_run_filter=flow_run_filter, limit=batch_size)
if not runs:
break
for fr in runs:
try:
await client.delete_flow_run(fr.id)
total_deleted += 1
except Exception as e:
logger.warning(f"Failed to delete {fr.id}: {e}")
await asyncio.sleep(0) # yield
<http://logger.info|logger.info>(f"Retention complete. Deleted {total_deleted} flow runs")
@flow(name="db-retention")
async def db_retention_flow(days_to_keep: int = 90):
await delete_old_top_level_flow_runs(days_to_keep=days_to_keep)
if __name__ == "__main__":
# Weekly on Sundays at 03:00
db_retention_flow.serve(
name="db-retention-weekly",
cron="0 3 * * 0",
parameters={"days_to_keep": 90},
)
# Alternatively, use a 7-day interval:
# from datetime import timedelta
# db_retention_flow.serve(name="db-retention-weekly", interval=timedelta(days=7))
When to choose which
- Prefer the built-in DB vacuum service if you just want reliable automatic cleanup. It also handles orphaned logs/artifacts and stale artifact collections.Marvin
02/26/2026, 5:58 PM