mark_deployments_ready updates every deployment on a queue on every worker poll
Bug summary
Every worker poll updates every deployment on the polled work pool's queues, whether or not anything changed. A queue with N deployments takes N row locks and N row updates per execution, continuously, with nothing scheduled or running.
get_scheduled_flow_runs submits mark_deployments_ready for the whole pool. The locked CTE takes FOR UPDATE on every deployment on those queues, and the UPDATE writes all of them with no predicate on current state:
sa.update(db.Deployment)
.where(db.Deployment.id.in_(select(locked.c.id)))
.values(
status=DeploymentStatus.READY,
last_polled=last_polled,
updated=db.Deployment.updated,
)unready_deployments, computed just above, is used only for event emission — not to narrow the write.
The only reader of last_polled is Foreman._mark_deployments_as_not_ready, which only checks whether it is older than deployment_last_polled_timeout_seconds (default 60).
With an idle work pool, n_tup_upd for deployment in pg_stat_user_tables climbs continuously in steps of N.
Suggested fix: still lock every row, but only write the ones that changed:
status != READY OR last_polled < now() - (deployment_last_polled_timeout_seconds / 2)The condition cannot go in the locked CTE — skipping the lock lets a concurrent stale NOT_READY transition win.
Raising PREFECT_WORKER_QUERY_SECONDS is not a workaround: docket.add(key=...) coalesces pending submissions, so execution rate is set by that dedup, not poll cadence.
Version info
Version: 3.8.6
API version: 0.8.4
Python version: 3.14.7
Server type: server
Server:
Database: postgresql
PostgreSQL version: 16Source: PrefectHQ/prefect