<@ULVA73B9P>, I have two Prefect deployments that ...
# marvin-ai
i
@Marvin, I have two Prefect deployments that both use a global concurrency limit of 1 (so only one run can execute at a time). Each run typically takes ~5 minutes, and each deployment is scheduled on a 10‑minute interval. What I’m seeing: deployment A keeps running normally, but deployment B ran once and then subsequent runs stay stuck in “Awaiting concurrency slot” and never seem to execute. • When a run is scheduled but can’t start due to concurrency, does Prefect apply any “stale/late” cutoff or timeout that eventually prevents the run from starting? • Is there a configurable setting like max lateness, schedule catch-up, deadline, max number of late scheduled runs, or similar that controls whether an “awaiting slot” run is still eligible to start later? • If the correct behavior is “it should run once a slot frees”, what are the common causes of it never starting (e.g., stuck RUNNING run holding the slot, work pool/worker concurrency vs deployment concurrency, queue/backlog behavior), and what should I check in the UI/API to confirm?
m
thought for 303.6 seconds
Thanks for the detailed context — that helps. Assuming Prefect 3.x: Short answers - Does “awaiting slot” ever time out? No. Prefect does not apply a hard “lateness cutoff” that stops a run from starting later. A run waiting for a concurrency slot remains eligible and will keep retrying. - Is there a setting like max lateness/catch‑up/deadline/max late runs that prevents starts? No. There’s no built‑in deadline or max backlog for concurrency‑gated runs. You can reduce backlog by using
collision_strategy=CANCEL_NEW
on the deployment (new runs are cancelled instead of queued), but there isn’t a “max lateness” or “catch‑up=False” toggle that disqualifies waiting runs. - If it “should run when a slot frees,” why would it never start? Most commonly: a run is continuously holding the slot (RUNNING/PENDING), queue/pool concurrency or priority is starving B, or B is waiting on a different limit key than you expect. See checks below. What “Awaiting concurrency slot” means - It’s a Scheduled state applied when a run can’t acquire a concurrency slot (deployment concurrency, global concurrency, or tag concurrency). Prefect reschedules the run to retry after a short delay (default ~30s, configurable). - It may also be marked Late after its scheduled time passes, but “Late” is informational/for SLAs — it does not block execution. Key settings and where they apply - Retry delay when a slot isn’t available: - Deployment concurrency retry cadence:
PREFECT_SERVER_DEPLOYMENTS_CONCURRENCY_SLOT_WAIT_SECONDS
(default ~30s) - Lease/hold behavior: - Deployment concurrency lease duration:
PREFECT_SERVER_CONCURRENCY_INITIAL_DEPLOYMENT_LEASE_DURATION
(default ~300s) - Grace period on deployments:
concurrency_options.grace_period_seconds
(default 300s). If infra spin‑up is slow, increase this so the slot isn’t released too early; if it’s too long and runs get stuck Pending, it can “feel” like starvation. - Late run marking (observability only): -
PREFECT_SERVER_SERVICES_LATE_RUNS_AFTER_SECONDS
(default 15s) Common reasons a run stays “Awaiting concurrency slot” - A RUNNING or PENDING run is continuously holding the slot - For global concurrency: another run (likely from deployment A) has the global limit lease most of the time. If A is generating backlog, it can starve B. - For deployment concurrency: a stuck RUNNING or long‑Pending run is holding the deployment’s slot. - Queue/pool concurrency or priority is starving B - Work pool/queue concurrency limits or queue priority can cause workers to keep pulling A’s runs first. - Workers aren’t pulling B’s runs - No worker attached to B’s pool/queue, or the worker is unhealthy. - Different concurrency keys than intended - If A and B don’t actually share the same global limit key, B may be waiting on a separate (possibly stuck) limit. - Collision strategy not enqueueing - If
collision_strategy=CANCEL_NEW
, new runs are cancelled instead of waiting (you’d see Cancelled, not Awaiting; mentioning it for completeness). What to check (UI/API) - Global concurrency usage - In the UI: Concurrency page (Global Concurrency Limits). Verify the limit name/key both deployments use, current in‑use slots, which flow run(s) hold them, and lease expiration. - Docs: Global concurrency limits - Deployment‑level concurrency settings - On each deployment:
concurrency_limit
,
concurrency_options.collision_strategy
,
grace_period_seconds
. Confirm they’re as expected and that you’re not also limiting at the deployment level in a way that causes double‑gating. - Docs: Deployments (concurrency section) - Active runs holding slots - Filter Flow Runs for the deployments and look for RUNNING/PENDING runs, then open a run and check the Concurrency/resources tab to see what it’s holding. - API example to list RUNNING/PENDING for a deployment: ```
curl -X POST "$PREFECT_API_URL/api/flow_runs/filter" \ -H "Content-Type: application/json" \ -d '{ "flow_runs": { "deployment_id": {"any_": ["<deployment_id>"]}, "state": {"type": {"any_": ["PENDING", "RUNNING"]}} } }' ``` - Work pool/queue limits and priority - Check the work pool’s and queue’s concurrency limits and queue priority. If A’s queue has higher priority and the pool limit is tight, B can be starved. - Verify a healthy worker is attached to B’s queue. - Docs: Work pools and queues - Late runs and SLAs (for observability) - If runs are marked Late, that’s expected during concurrency waits; it doesn’t block starting. - Docs: States and SLAs Why you might see A running regularly and B “never” starting - If A’s schedule creates a steady backlog (or A’s queue has higher priority), the worker may continually service A’s runs. Since the global limit is 1, B keeps retrying acquisition but loses the race most of the time. - If A and B are scheduled simultaneously, one may consistently beat the other to the slot. A small schedule offset (e.g., start B 5 minutes after A) often restores fairness. - If there’s a stuck run (RUNNING or long‑PENDING) holding the slot, B won’t get a turn until it’s released — check A’s and B’s run history and the concurrency page for the holder. Practical next steps - Verify both deployments reference the same global concurrency key and confirm which runs are currently holding it (Concurrency page). - Check queue/pool concurrency and queue priority. Ensure B’s queue has a worker and is not deprioritized. - Reduce starvation by offsetting schedules (e.g., A at 00/10/:20, B at 05/15/:25). - If you want to prevent backlog growth instead of waiting, set
collision_strategy=CANCEL_NEW
on the deployments. - Tune
grace_period_seconds
to match infra startup so slots aren’t held too long by Pending runs. - If you suspect a stuck lease/run, cancel that run or stop/restart the worker; leases that aren’t renewed will expire, releasing the slot. References - States: States - Schedules: Schedules - SLAs: SLAs - Deployments (concurrency): Deployments - Global concurrency limits: Global concurrency limits - Work pools/queues and queue priority: Work pools - Settings reference (late run window, concurrency wait/lease settings): Settings reference If you can share: - whether A and B definitely share the same global limit key, - their queue priorities and work pool limits, - and a screenshot of the Concurrency page showing who holds the slot, I can help pinpoint exactly why B is getting starved and suggest the minimal change to fix it.
i
@Marvin @Marvin, can you confirm the correct behavior of Prefect 3.x when multiple deployments share a global concurrency limit=1: does the scheduler guarantee fairness between deployments, or can one deployment starve another indefinitely if their runs are scheduled at the same time? If starvation is possible, what’s the recommended mitigation—schedule offset via anchor_date, or a supported fairness/priority mechanism? If you want, I can also patch the dashboard “Cancel all” to use Prefect’s real /set_state cancellation so it actually works on your server.
m
thought for 160.6 seconds
Great question — and yes, this can definitely bite you. Direct answer - Prefect 3.x does not guarantee fairness or round‑robin between deployments that share a global concurrency limit (e.g., limit=1). If their runs contend at the same time, one deployment can effectively starve the other. - Workers pull runs in priority order (by work queue priority) then by earliest scheduled time. There’s no fairness queue per concurrency limit; it’s first-come-first-served at the instant a slot frees. - So if A routinely grabs the slot first (or has higher queue priority), B can sit in “Awaiting concurrency slot” for a long time. Recommended mitigations - Easiest: offset the schedules so they don’t collide. Stagger B’s schedule a few minutes off of A (anchor_date/offset) so their next_scheduled_start_time values aren’t identical. - Use work queue priorities: put A and B on separate queues and assign priorities intentionally if you want a deterministic winner. Lower number = higher priority. - Separate queues per deployment: this makes priority explicit and lets you reason about pull order. Keep them in the same work pool if you want shared worker capacity. - If you want to prevent backlog rather than wait, set deployment
collision_strategy=CANCEL_NEW
so new runs are cancelled when the limit is saturated (instead of enqueued in “awaiting slot”). - Consider whether a single global limit is necessary; per‑deployment limits or different global keys can reduce cross‑deployment contention. Why starvation happens - Slot acquisition is atomic and first‑come‑first‑served; there’s no round‑robin across deployments. - Worker SQL ordering favors: 1) work queue priority, 2) earliest next_scheduled_start_time, 3) tie-break by insertion/ID timing. - If A and B schedule at the same moment, tiny timing differences (or higher priority for A’s queue) can cause A to repeatedly win. Docs for deeper reference - Global concurrency limits: Global concurrency limits - Work pools/queues and priority: Work pools - Deployments (concurrency options): Deployments - States (Late/Awaiting slot): States - Settings reference (slot wait, lease durations): Settings reference Re: “Cancel all” in the dashboard Totally appreciate the offer. If you’ve found a case where the bulk cancel UI isn’t using the proper /set_state pathway (or not propagating to the server state machine), that’s worth fixing. If you can share a brief repro or a tiny patch, I can flag it to the team or open a PR/discussion to get it reviewed.