Andrea Piccione
04/08/2026, 9:14 AMempirical_policy to run_deployment so I can add retries.
Relevant part of the code :
result: Coroutine[Any, Any, FlowRun] = run_deployment(
name=flow_name,
parameters=kwargs,
as_subflow=True,
job_variables=job_variables,
flow_run_name=flow_run_name,
idempotency_key=idempotency_key,
timeout=5, # give 5s to make the flow run and id which we can use to get the logs
) # type: ignore
flow_run: FlowRun = await result
state = await wait_for_state(flow_run, raise_exception_on_failure=raise_exception_on_failure)Marvin
04/08/2026, 9:14 AMMarvin
04/08/2026, 9:18 AMprefect.deployments.run_deployment does not accept an empirical_policy argument. Retries for a subflow started via run_deployment must be configured on the called flow/deployment or applied to the created flow run after the fact.
Your options:
- Preferable: set retries on the called flow
from prefect import flow
@flow(retries=3, retry_delay_seconds=30)
def child_flow(...):
...
This will apply no matter how the deployment is triggered (including as a subflow on ECS).
- Programmatically override right after creating the flow run
from prefect.deployments import run_deployment
from prefect.client.orchestration import get_client
from prefect.client.schemas.objects import FlowRunPolicy
# Create the subflow run (return immediately so we can update it)
flow_run = await run_deployment(
name=flow_name,
parameters=kwargs,
as_subflow=True,
job_variables=job_variables,
flow_run_name=flow_run_name,
idempotency_key=idempotency_key,
timeout=0,
)
# Set/override retry policy on the flow run
policy = FlowRunPolicy(max_retries=3, retry_delay_seconds=30, retry_type="reschedule")
async with get_client() as client:
await client.update_flow_run(flow_run.id, empirical_policy=policy)
# now wait for completion if you like
state = await wait_for_state(flow_run, raise_exception_on_failure=raise_exception_on_failure)
Tip: using timeout=0 or scheduling the run a few seconds in the future can reduce race conditions so the policy is set before the first attempt. For ECS, retry_type="reschedule" is typically what you want so each retry is a new scheduled attempt picked up by a worker.
- As a separate pattern: use an Automation to “retry” by running the deployment again on failure. This is event-driven and outside the flow engine’s in-process retry loop.
References:
- Flow retries how-to: Retries guide
- Retrying flow runs (manually/automation): Retry flow runs
- API reference for `run_deployment`: run_deployment API
- FlowRunPolicy schema: FlowRunPolicyAndrea Piccione
04/08/2026, 12:41 PMAndrea Piccione
04/08/2026, 2:08 PMMarvin
04/08/2026, 2:08 PMMarvin
04/08/2026, 2:09 PMAndrea Piccione
04/09/2026, 9:27 AMretry_type if I leave it blank? Because I have never set it when using the @flow decoratorMarvin
04/09/2026, 9:28 AMMarvin
04/09/2026, 9:30 AMretry_type on `@flow`—and if you leave it “blank,” its default is None.
Details (Prefect 3.x):
- @flow does not accept a retry_type argument; you configure retries, retry_delay_seconds, and retry_jitter_factor instead.
- retry_type is an internal flow-run field. Its default is None when the run is not being retried.
- When Prefect automatically retries a failed flow (because retries > 0), orchestration sets retry_type to in_process.
- If you manually rerun/reschedule a terminal flow run from the UI/API, orchestration sets retry_type to reschedule.
Links (source):
- FlowRunPolicy schema showing retry_type default `None`: https://github.com/PrefectHQ/prefect/blob/75a10ad4aab57304d21bf261f2eb174a137ff56e/src/prefect/server/schemas/core.py#L130-L132
- Orchestration setting retry_type="in_process" on automatic retries: https://github.com/PrefectHQ/prefect/blob/75a10ad4aab57304d21bf261f2eb174a137ff56e/src/prefect/server/orchestration/core_policy.py#L1098-L1105
- Orchestration setting retry_type="reschedule" on manual reruns: https://github.com/PrefectHQ/prefect/blob/75a10ad4aab57304d21bf261f2eb174a137ff56e/src/prefect/server/orchestration/core_policy.py#L1618-L1625
FYI: tasks don’t have a retry_type field—only flows track this.Andrea Piccione
04/09/2026, 10:28 AMLate state.
I am running this as concurrent subflows on a ECS work poolMarvin
04/09/2026, 10:28 AMMarvin
04/09/2026, 10:35 AM0 is treated as “no timeout” (it becomes None internally), so it won’t force a stop. It won’t cause a Late state either.
- If you mean `run_deployment(..., timeout=0, as_subflow=True)`: timeout=0 just makes the parent return immediately after creating the subflow run; it does not affect scheduling or execution. It’ll rapidly queue a lot of subflow runs.
What “Late” means here:
- “Late” is a Scheduled state that the server sets when a run has not started by its scheduled time plus a small grace period (15s by default). It’s not a terminal/hanging state; it’s “scheduled but hasn’t started yet.” Code reference: https://github.com/PrefectHQ/prefect/blob/75a10ad4aab57304d21bf261f2eb174a137ff56e/src/prefect/server/orchestration/core_policy.py#L1704-L1723
Why you’re likely seeing “Late” with concurrent subflows on ECS:
- With timeout=0 on run_deployment, your parent flow will enqueue many subflow runs very quickly.
- If your ECS work pool/cluster doesn’t have enough capacity (or the worker isn’t polling the right pool/queue), those runs won’t start promptly and will flip to Late after the grace period.
- ECS startup overhead (registering task definitions, pulling images, capacity constraints) can add seconds to startup; if there’s a backlog, runs can remain Late until capacity frees up.
Checks to run:
- Verify the worker is online and polling the correct pool/queue:
prefect work-pool inspect "<your-ecs-pool>"
Look for “paused: false”, current concurrency limit, and assigned queues.
- Confirm there’s an ECS worker running and pointed at that pool:
prefect worker start -p "<your-ecs-pool>"
- Check deployment settings for the subflows: they must target the ECS work pool you expect (and the right work queue).
- Verify ECS capacity (CPU/memory), task definition registration logs, VPC/subnet/security groups. If capacity is tight, scale the cluster or reduce each task’s CPU/memory request.
Options to mitigate:
- Scale up ECS capacity and/or run more ECS workers.
- Throttle how many subflows you launch at once (e.g., batch calls to run_deployment and wait for some to start/finish before launching more).
- If you just don’t want to see “Late” for short startup delays, increase the late-run threshold on the server:
export PREFECT_SERVER_SERVICES_LATE_RUNS_AFTER_SECONDS=30
(Default is 15 seconds.)
- If you intended an actual execution timeout for the subflow, set a positive timeout_seconds on the subflow’s flow decorator (e.g., @flow(timeout_seconds=300)). Don’t use `0`—that disables timeouts.
If runs remain stuck in Late and never transition to Running, that usually means the worker isn’t able to start them. Check the ECS worker logs for task creation/registration retries and ensure the pool/queue names match.
Happy to dig in further—could you share:
- How you’re launching subflows (a small snippet), especially whether you’re using run_deployment(..., as_subflow=True, timeout=0).
- The ECS work pool name and prefect work-pool inspect output.
- One affected flow run ID and the ECS worker logs around the time it should have started.Andrea Piccione
04/09/2026, 10:37 AMrun_deployment(..., timeout=0, as_subflow=True) .
I want to be able to set timeout to something like 5 seconds, but also update the FlowRunPolicyMarvin
04/09/2026, 10:37 AMMarvin
04/09/2026, 10:38 AMfrom prefect import flow, get_client
from prefect.deployments import run_deployment
@flow
def parent():
# Launch child as a subflow and wait up to 5s for completion
child_run = run_deployment(
name="my-child-flow/my-child-deployment",
parameters={"x": 1},
as_subflow=True,
timeout=5, # wait up to 5 seconds
poll_interval=1, # optional, faster polling
# job_variables=... # if you need ECS-specific overrides
)
# Update the subflow’s empirical policy (retries/retry_delay)
# Note: retry_type cannot be set by users.
with get_client() as client:
client.update_flow_run(
flow_run_id=child_run.id,
empirical_policy={
"retries": 2, # number of retries
"retry_delay": 30, # seconds between retries
},
)
return child_run.id
Notes and caveats
- If the child finishes within 5s, great — you’ll get its final state back. If not, you still get the FlowRun object; it continues running on ECS.
- Updating empirical_policy right after creation is safe; Prefect will respect it for subsequent retries.
- You cannot set retry_type directly; it’s set by orchestration depending on whether it’s an automatic retry (in_process) or a reschedule/reenqueue (reschedule).
If you’d prefer to ensure the policy is applied before the child starts running, we can switch to using the client to create the run and set the policy up front, then return to the parent — but run_deployment’s convenience function doesn’t expose empirical_policy at creation time. If you want that route, I can share a snippet that mirrors run_deployment behavior manually and sets empirical_policy during creation.Andrea Piccione
04/09/2026, 10:40 AMMarvin
04/09/2026, 10:40 AMMarvin
04/09/2026, 10:42 AMimport anyio
from prefect import get_client
from prefect.client.schemas.objects import FlowRunPolicy
from uuid import UUID
async def launch_subflow_with_policy(
deployment_id: UUID,
*,
parameters: dict | None = None,
tags: list[str] | None = None,
job_variables: dict | None = None,
name: str | None = None,
# empirical policy you want to enforce
retries: int | None = None,
retry_delay: int | None = None, # seconds
# emulate run_deployment timeout/polling behavior
timeout: float | None = 5,
poll_interval: float = 1.0,
):
async with get_client() as client:
# 1) Create the flow run from the deployment
flow_run = await client.create_flow_run_from_deployment(
deployment_id=deployment_id,
parameters=parameters,
tags=tags,
name=name,
job_variables=job_variables,
# you can pass work_queue_name if you need to target a specific queue
)
# 2) Immediately update the run’s empirical policy (retries/retry_delay)
# retry_type is not settable by users.
if retries is not None or retry_delay is not None:
await client.update_flow_run(
flow_run_id=flow_run.id,
empirical_policy=FlowRunPolicy(
retries=retries,
retry_delay=retry_delay,
),
)
# 3) Optional: poll like run_deployment(timeout=...)
if timeout == 0:
return flow_run
if timeout is None:
# poll indefinitely until final state
while True:
fr = await client.read_flow_run(flow_run.id)
if fr.state and fr.state.is_final():
return fr
await anyio.sleep(poll_interval)
else:
# poll up to timeout seconds
with anyio.move_on_after(timeout):
while True:
fr = await client.read_flow_run(flow_run.id)
if fr.state and fr.state.is_final():
return fr
await anyio.sleep(poll_interval)
# timed out waiting; return latest snapshot
return await client.read_flow_run(flow_run.id)
Sync version
```
import time
from prefect import get_client
from prefect.client.schemas.objects import FlowRunPolicy
from uuid import UUID
def launch_subflow_with_policy_sync(
deployment_id: UUID,
*,
parameters: dict | None = None,
tags: list[str] | None = None,
job_variables: dict | None = None,
name: str | None = None,
retries: int | None = None,
retry_delay: int | None = None, # seconds
timeout: float | None = 5,
poll_interval: float = 1.0,
):
with get_client(sync_client=True) as client:
flow_run = client.create_flow_run_from_deployment(
deployment_id=deployment_id,
parameters=parameters,
tags=tags,
name=name,
job_variables=job_variables,
)
if retries is not None or retry_delay is not None:
client.update_flow_run(
flow_run_id=flow_run.id,
empirical_policy=FlowRunPolicy(
retries=retries,
retry_delay=retry_delay,
),
)
if timeout == 0:
return flow_run
start = time.monotonic()
while True:
fr = client.read_flow_run(flow_run.id)Marvin
04/09/2026, 10:42 AMAndrea Piccione
04/09/2026, 12:39 PMMarvin
04/09/2026, 12:40 PMMarvin
04/09/2026, 12:42 PMMarvin
04/09/2026, 12:42 PMOption B — Set empirical_policy at creation (advanced)
- Same subflow linkage as above, but we POST our own DeploymentFlowRunCreate (including empirical_policy) to the API directly. This applies the policy “during creation.”
import time
import uuid
from prefect import get_client
from prefect.context import FlowRunContext, TaskRunContext
from prefect.tasks import Task
from prefect.states import Pending
from prefect.client.schemas.objects import FlowRunPolicy, FlowRun
from prefect.client.schemas.actions import DeploymentFlowRunCreate, StateCreate
from prefect.states import Scheduled
from datetime import datetime, timezone
from prefect.engine import collect_task_run_inputs_sync
def launch_subflow_with_policy_during_creation(
*,
deployment_id, # UUID
parameters: dict | None = None,
tags: list[str] | None = None,
job_variables: dict | None = None,
name: str | None = None,
work_queue_name: str | None = None,
# policy to apply at creation time
retries: int | None = None,
retry_delay: int | None = None, # seconds
timeout: float | None = 5,
poll_interval: float = 1.0,
):
parameters = parameters or {}
flow_ctx = FlowRunContext.get()
task_ctx = TaskRunContext.get()
if not (flow_ctx or task_ctx):
raise RuntimeError("Must be called from within a flow or task to create a subflow.")
flow_run_id = flow_ctx.flow_run.id if flow_ctx else task_ctx.task_run.flow_run_id
dummy_task = Task(name=name or "subflow", fn=lambda: None, version=None)
dynamic_key = f"subflow-{uuid.uuid4().hex[:8]}"
task_inputs = {k: collect_task_run_inputs_sync(v) for k, v in parameters.items()}
if task_ctx and (not flow_ctx or task_ctx.task_run.flow_run_id == getattr(flow_ctx.flow_run, "id", None)):
from prefect.client.schemas.objects import TaskRunResult
task_inputs["__parents__"] = [TaskRunResult(id=task_ctx.task_run.id)]
with get_client(sync_client=True) as client:
parent_task = client.create_task_run(
task=dummy_task,
flow_run_id=flow_run_id,
dynamic_key=dynamic_key,
task_inputs=task_inputs,
state=Pending(),
)
# Build the body we’ll POST directly to the deployments endpoint
body = DeploymentFlowRunCreate(
parameters=parameters,
tags=tags or [],
name=name,
parent_task_run_id=parent_task.id,
work_queue_name=work_queue_name,
job_variables=job_variables,
# set empirical_policy at creation
empirical_policy=FlowRunPolicy(retries=retries, retry_delay=retry_delay),
# replicate run_deployment's immediate scheduling
state=StateCreate.from_state(Scheduled(scheduled_time=datetime.now(timezone.utc))),
).model_dump(mode="json", exclude_unset=True)
# POST: /deployments/{id}/create_flow_run
resp = client.request(
"POST",
"/deployments/{id}/create_flow_run",
path_params={"id": deployment_id},
json=body,
)
child = FlowRun.model_validate(resp.json())
if timeout == 0:
return child
start = time.monotonic()
while True:
fr = client.read_flow_run(child.id)
if fr.state and fr.state.is_final():
return fr
if timeout is not None and (time.monotonic() - start) >= timeout:
return fr
time.sleep(poll_interval)
```
Key pointsMarvin
04/09/2026, 12:42 PMAndrea Piccione
04/09/2026, 12:58 PMresult: Coroutine[Any, Any, FlowRun] = run_deployment(
name=flow_name,
parameters=kwargs,
as_subflow=True,
job_variables=job_variables,
flow_run_name=flow_run_name,
idempotency_key=idempotency_key,
timeout=5,
poll_interval=1,
) # type: ignore
flow_run = await result
policy = FlowRunPolicy(max_retries=2, retry_delay_seconds=30, retry_type="reschedule")
async with get_client() as client:
await client.update_flow_run(flow_run.id, empirical_policy=policy)
This code is called by 6 concurrent subflows.
What I am seeing is that 4 transition from Scheduled to Pending to Running, whilst 2 remain in Late stateMarvin
04/09/2026, 12:58 PMMarvin
04/09/2026, 12:59 PMfrom prefect.client.schemas.objects import FlowRunPolicy
policy = FlowRunPolicy(retries=2, retry_delay=30)
async with get_client() as client:
await client.update_flow_run(flow_run.id, empirical_policy=policy)
Schema reference showing current fields (and presence of empirical_policy during creation): https://github.com/PrefectHQ/prefect/blob/main/src/prefect/client/schemas/actions.py#L525-L589
3) Why 4 start and 2 remain Late
- “Late” means “still Scheduled and missed its scheduled_start_time by the grace period” (15s by default). It’s typically due to:
- ECS capacity/scale-up delay (common when launching several tasks simultaneously)
- Work pool or deployment concurrency limits
- Slower task definition registration or image pulls for some of the runs
- They should still transition out of Late once a worker starts them, unless something is blocking those two from being picked up. Late state rule: https://github.com/PrefectHQ/prefect/blob/75a10ad4aab57304d21bf261f2eb174a137ff56e/src/prefect/server/orchestration/core_policy.py#L1704-L1723
- Late threshold settings (default after_seconds=15): https://github.com/PrefectHQ/prefect/blob/main/src/prefect/settings/models/server/services.py (ServerServicesLateRunsSettings)
4) Make the policy stick before pickup (and reduce Late)
If you want to guarantee the empirical_policy is applied at creation time (and avoid the race), create the subflow with parent_task_run_id and set empirical_policy during creation. You can also push the scheduled_time a few seconds into the future to avoid Late while ECS spins up:
```
from datetime import datetime, timezone, timedelta
from prefect import get_client
from prefect.context import FlowRunContext, TaskRunContext
from prefect.tasks import Task
from prefect.states import Pending, Scheduled
from prefect.client.schemas.objects import FlowRunPolicy, FlowRun
from prefect.client.schemas.actions import DeploymentFlowRunCreate, StateCreate
async def launch_subflow_with_policy_now(
deployment_id,
parameters=None,
name=None,
tags=None,
job_variables=None,
work_queue_name=None,
retries=2,
retry_delay=30,
cushion_seconds=10, # schedule a few seconds in the future to avoid Late
):
flow_ctx = FlowRunContext.get()
task_ctx = TaskRunContext.get()
if not (flow_ctx or task_ctx):
raise RuntimeError("Call from within a flow/task to create a true subflow.")
flow_run_id = flow_ctx.flow_run.id if flow_ctx else task_ctx.task_run.flow_run_id
# 1) Create a parent placeholder task run (subflow linkage)
dummy_task = Task(name=name or "subflow", fn=lambda: None, version=None)
async with get_client() as client:
parent_task = await client.create_task_run(
task=dummy_task,
flow_run_id=flow_run_id,
dynamic_key=f"subflow-{datetime.now().timestamp()}",
task_inputs={},
state=Pending(),
)
# 2) Create the child with empirical_policy set at creation, and a slight schedule cushion
body = DeploymentFlowRunCreate(Marvin
04/09/2026, 12:59 PM5) Troubleshooting checklist for the two Late runs
- Check pool and queue:
- Is the ECS work pool paused or at its concurrency limit?
- Are there enough workers polling that pool/queue?
prefect work-pool inspect "<your-ecs-pool>"
- Check deployment concurrency limit and strategy (may gate starts).
prefect deployment inspect "<flow-name>/<deployment-name>"
- Check ECS capacity/logs:
- Are corresponding ECS tasks being created for those two runs?
- Any capacity/provider constraints, task-definition registration retries, or image pull delays?
- If Late is purely cosmetic for short start delays, consider increasing the Late threshold:
export PREFECT_SERVER_SERVICES_LATE_RUNS_AFTER_SECONDS=30
```
TL;DR
- Use FlowRunPolicy(retries=..., retry_delay=...) and don’t set retry_type.
- timeout=5 just controls how long you wait; it doesn’t influence scheduling.
- Two “Late” runs usually indicate ECS capacity or concurrency constraints; they should start once capacity frees up.
- If you want the policy applied before pickup (and to reduce Late), use the “create as subflow + set empirical_policy during creation” pattern above and optionally schedule a few seconds in the future.Andrea Piccione
04/09/2026, 1:06 PMMarvin
04/09/2026, 1:06 PMMarvin
04/09/2026, 1:09 PMfrom prefect.client.schemas.objects import FlowRunPolicy
policy = FlowRunPolicy(retries=2, retry_delay=30) # do not set retry_type
async with get_client() as client:
await client.update_flow_run(flow_run.id, empirical_policy=policy)
This works while the run is Scheduled (or any state). It does not affect scheduling, but it configures retries when failures occur.
Why two subflows are stuck in Late and never start
- Late is a Scheduled variant set when a run hasn’t started by its scheduled time plus a grace period (default ~15s). It will only move out of Late when a worker picks it up.
- If some runs never leave Late, there’s almost always a pickup/blocking issue, not a policy issue. Common blockers:
- Pool or queue paused
- No worker polling that pool/queue
- Deployment concurrency limit is full
- With CANCEL_NEW collision strategy: Late runs can be proactively Cancelled instead of staying Late
- With ENQUEUE: Late runs may remain Late until a slot frees up
- ECS capacity constraints (insufficient CPU/memory, scaling delays), task-def registration/image pulls slower for some tasks
- Worker query filters exclude certain runs, e.g., runs with empirical_policy.retry_type='in_process' are excluded from queue selection
Concrete checks to run
- Verify the pool is healthy and not paused:
prefect work-pool inspect "<your-ecs-pool>"
Look for paused=false, a non-zero concurrency limit if set, and that workers are connected.
- Inspect the deployment for a concurrency limit and collision strategy:
prefect deployment inspect "<flow-name>/<deployment-name>"
If the limit is small and the strategy is CANCEL_NEW, Late runs can be cancelled at mark-late time; with ENQUEUE, they can remain Late until a slot frees.
- Confirm a worker is polling the right pool/queue and is healthy:
prefect worker start -p "<your-ecs-pool>"
Then check worker logs for:
- Task definition registration retries or failures
- Image pull delays
- Capacity provider issues (Fargate/EC2)
- Whether those specific run IDs are ever requested
- If you rely on work queues, ensure you target the correct queue in run_deployment (work_queue_name) and that the queue is not paused.
- If Late is caused by brief ECS startup delays and is only cosmetic, consider increasing the Late grace threshold:
export PREFECT_SERVER_SERVICES_LATE_RUNS_AFTER_SECONDS=30
This won’t fix “never picked up” runs, but reduces false Late during bursts.
Key point about empirical_policy vs Late
- Updating empirical_policy does not move a run out of Late; it’s just metadata for retries once the run actually starts.
- If runs never leave Late, a worker/pool/queue/concurrency/capacity condition is preventing pickup. Fixing that is the path forward.
If you can share:
- The ECS work pool name and output of prefect work-pool inspect
- The deployment’s concurrency settings and collision strategy
- One affected flow run ID
I can identify the exact blocker.Andrea Piccione
04/09/2026, 1:40 PMMarvin
04/09/2026, 1:40 PMMarvin
04/09/2026, 1:42 PMexport PREFECT_API_SERVICES_LATE_RUNS_AFTER_SECONDS=30
prefect server start
- No change is needed on ECS workers.