#23129·prefect

mark_deployments_ready updates every deployment on a queue on every worker poll

Author: chuqCTCCreated Sep 15, 2026Updated Sep 16, 2026
Labelsenhancement

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:

python
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: 16