Scott Pedersen
11/05/2025, 8:07 PMMarvin
11/05/2025, 8:07 PMMarvin
11/05/2025, 8:17 PMMarvin
11/05/2025, 8:17 PMScott Pedersen
11/05/2025, 8:22 PMMarvin
11/05/2025, 8:22 PMMarvin
11/05/2025, 8:24 PMfrom datetime import datetime, timedelta, timezone
from prefect import flow
from prefect.blocks.notifications import SlackWebhook
from prefect.client.orchestration import get_client
from prefect.client.schemas.filters import (
TaskRunFilter,
TaskRunFilterState,
TaskRunFilterStateName,
TaskRunFilterTags,
FlowRunFilter,
FlowRunFilterState,
FlowRunFilterStateName,
)
THRESHOLD = timedelta(minutes=60)
@flow
async def alert_on_long_waits_v2(
slack_block_name: str = "my-slack-webhook",
task_tags_to_watch: list[str] | None = None,
check_task_runs: bool = True,
check_flow_runs: bool = False, # set True if you want to also flag long-scheduled flow runs (work pool/queue pressure)
):
slack = await SlackWebhook.load(slack_block_name)
now = datetime.now(timezone.utc)
async with get_client() as client:
if check_task_runs:
tag_filter = None
if task_tags_to_watch:
tag_filter = TaskRunFilterTags(all_=task_tags_to_watch)
trs = await client.read_task_runs(
task_run_filter=TaskRunFilter(
state=TaskRunFilterState(
name=TaskRunFilterStateName(any_=["Pending"])
),
tags=tag_filter,
)
)
for tr in trs:
if tr.state and tr.state.timestamp and (now - tr.state.timestamp) > THRESHOLD:
msg_extra = f" | msg: {tr.state.message}" if (tr.state and tr.state.message) else ""
await slack.notify(
body=(
f"Task run '{tr.name}' has been Pending for "
f"{(now - tr.state.timestamp)} (likely waiting on tag concurrency). "
f"Task run ID: {tr.id} | Flow run ID: {tr.flow_run_id}{msg_extra}"
)
)
if check_flow_runs:
frs = await client.read_flow_runs(
flow_run_filter=FlowRunFilter(
state=FlowRunFilterState(
name=FlowRunFilterStateName(any_=["Scheduled"])
)
)
)
for fr in frs:
if fr.state and fr.state.timestamp and (now - fr.state.timestamp) > THRESHOLD:
await slack.notify(
body=(
f"Flow run '{fr.name}' has been Scheduled for "
f"{(now - fr.state.timestamp)} (likely due to work pool/queue concurrency). "
f"Flow run ID: {fr.id}"
)
)
Option B — Prefect Cloud 2.x Automations
If you’re on Cloud 2.x, you can build an Automation that:Marvin
11/05/2025, 8:24 PMScott Pedersen
11/05/2025, 8:27 PMMarvin
11/05/2025, 8:27 PMMarvin
11/05/2025, 8:28 PMconcurrency-monitored to those tasks so you can filter reliably.
3) Make it Proactive:
- Start “after” the task run enters Pending.
- “Expect” a state change (e.g., to Running or any non-Pending state) within 60 minutes.
- Evaluate “for each” task run (so one stuck task doesn’t suppress others).
4) Action: SendNotification using your Slack webhook block.
This will send Slack when a task run remains Pending for > 60 minutes. Because Pending can mean a few things in 2.x (not just concurrency), the tag filter is important to keep the alert specific to the tasks governed by your tag-based concurrency.
Helpful Prefect 2.x docs:
- Automations (2.x)
- Task tags and concurrency (2.x)
- Events (2.x)
Option B — A small monitoring flow you deploy and schedule
If you prefer code (or want more control), schedule this flow to run every few minutes. It finds task runs that have been Pending longer than 60 minutes and sends a Slack message. You can scope it to tags matching your pattern (or again, add a stable secondary tag to filter on).
from datetime import datetime, timedelta, timezone
from prefect import flow
from prefect.blocks.notifications import SlackWebhook
from prefect.client.orchestration import get_client
from prefect.client.schemas.filters import (
TaskRunFilter,
TaskRunFilterState,
TaskRunFilterStateName,
TaskRunFilterTags,
)
THRESHOLD = timedelta(minutes=60)
@flow
async def alert_on_long_pending_tasks(
slack_block_name: str = "my-slack-webhook",
# e.g. provide a stable tag these tasks carry in addition to your unique tag pattern
tags_to_watch: list[str] | None = None,
):
slack = await SlackWebhook.load(slack_block_name)
now = datetime.now(timezone.utc)
async with get_client() as client:
tag_filter = TaskRunFilterTags(all_=tags_to_watch) if tags_to_watch else None
task_runs = await client.read_task_runs(
task_run_filter=TaskRunFilter(
state=TaskRunFilterState(
name=TaskRunFilterStateName(any_=["Pending"])
),
tags=tag_filter,
)
)
for tr in task_runs:
if tr.state and tr.state.timestamp and (now - tr.state.timestamp) > THRESHOLD:
# Optional: try to reduce false positives by checking message text
msg_hint = f" | msg: {tr.state.message}" if tr.state.message else ""
await slack.notify(
body=(
f"Task run '{tr.name}' has been Pending for "
f"{(now - tr.state.timestamp)} (likely waiting on tag concurrency). "
f"Task run ID: {tr.id} | Flow run ID: {tr.flow_run_id}{msg_hint}"
)
)
Notes and tips:Marvin
11/05/2025, 8:28 PMconcurrency-monitored) to those tasks so you can filter on it in either Automations or the code above.
- You can tune how often tasks retry acquiring a concurrency slot with PREFECT_TASK_RUN_TAG_CONCURRENCY_SLOT_WAIT_SECONDS, but that won’t change the 60-minute alert logic.
If you eventually upgrade to Prefect 3.x, concurrency waits show up explicitly as the AwaitingConcurrencySlot state, which makes Automations and queries more precise.
If you want, share:
- The tag prefix/pattern you’re using,
- Whether you prefer a UI Automation or a code-based monitor,
and I’ll give you a ready-to-apply configuration or a deployment-ready flow tailored to your tags.