Olve Heitmann
10/26/2025, 9:52 PMMarvin
10/26/2025, 9:52 PMMarvin
10/26/2025, 9:58 PM# get infra pid (container id) from Prefect
prefect flow-run inspect <FLOW_RUN_ID>
# then check Docker
docker ps -a | grep <CONTAINER_ID>
docker logs --tail 200 <CONTAINER_ID>
# if still running or stuck, stop and remove
docker stop <CONTAINER_ID> && docker rm <CONTAINER_ID>
If the container is gone or stopped, it’s not using CPU; removing it ensures no disk/memory is tied up by an exited container.
- Prefer Cancel over Delete in the UI/CLI
- Cancel will signal the worker to kill infrastructure; Delete only removes the record.
- CLI for reference:
prefect flow-run cancel <FLOW_RUN_ID>
prefect flow-run delete <FLOW_RUN_ID>
Hardening to prevent/recover from zombies
- Enable runner heartbeats so Prefect can detect stalls and auto-recover
- Set an env var in your Docker job to emit periodic heartbeats:
- PREFECT_RUNNER_HEARTBEAT_FREQUENCY=60 (any integer ≥ 30 enables heartbeats)
- Create an Automation to act when heartbeats are missed (two common choices):
- Mark as Crashed when no heartbeat is seen within your window (e.g., ~2–3 minutes)
- Or Cancel the run to ensure infrastructure is killed
- UI: Automations -> New automation -> Proactive -> “When a flow run heartbeat is missing …” -> Action: Set state to Crashed (or Cancel)
- Docs: Detect zombie flows, Event triggers
- Ensure containers are cleaned up automatically
- Set auto_remove: true in your Docker worker job configuration so exited containers are removed by Docker:
- In the Work Pool UI: edit your Docker pool’s base job template and set auto_remove to true
- Or per-deployment with job_variables
- Example deployment (Prefect 3.x):
from prefect import flow
@flow
def my_flow():
...
if __name__ == "__main__":
my_flow.from_source(
source="<https://github.com/your-org/your-repo>",
entrypoint="flows/my_flow.py:my_flow",
).deploy(
name="my-docker-deployment",
work_pool_name="your-docker-pool",
job_variables={
"image": "your-image:latest",
"auto_remove": True,
"env": {"PREFECT_RUNNER_HEARTBEAT_FREQUENCY": "60"},
},
)
- Docker worker config reference: auto_remove is supported by the Docker worker job configuration.
- Add a “long running” safety net
- If you know how long a flow should run, set a flow-level timeout:
@flow(timeout_seconds=3600)
def my_flow():
...
- You can also add an Automation: “When a flow run has been Running for > X hours -> Cancel”.
- Increase observability for the next occurrence
- Set worker logs to DEBUG to capture more around submission/teardown:
export PREFECT_LOGGING_LEVEL=DEBUG
prefect worker start -p <POOL_NAME>
- When you see a zombie, check:
- Worker logs around the run’s start/finish
- Container logs (docker logs <CONTAINER_ID>)
- Whether the flow ever logged a flow-level completion (e.g., “Flow run finished in state Completed(...)”) — if your last log is a task completion, the engine likely crashed or lost connectivity after tasks completed but before finalizing the flow state.
Likely causes to investigate
- Container/engine exits or network blip after the last task finishes but before the engine posts the final flow state.Marvin
10/26/2025, 9:58 PMauto_remove: true on your Docker jobs?
If you can share a flow run ID and your worker logs around the end of that run, I can help trace exactly where it stalled.Olve Heitmann
10/26/2025, 10:06 PMMarvin
10/26/2025, 10:06 PMMarvin
10/26/2025, 10:07 PMinfrastructure_pid shows the container id:
- From your parent run:
infrastructure_pid='<http+docker://localhost:0>f3cd5bd56d434d7192965fab51d060c2e18b5b0f8d31d8a0f8fe0f9acbe9f18'
The container id is the long suffix:
f3cd5bd56d434d7192965fab51d060c2e18b5b0f8d31d8a0f8fe0f9acbe9f18
- Check its status/logs:
docker ps -a --no-trunc --filter id=f3cd5bd56d434d7192965fab51d060c2e18b5b0f8d31d8a0f8fe0f9acbe9f18
docker logs --tail 200 f3cd5bd56d434d7192965fab51d060c2e18b5b0f8d31d8a0f8fe0f9acbe9f18
docker inspect -f '{{.State.Status}} Exit={{.State.ExitCode}} OOMKilled={{.State.OOMKilled}}' f3cd5b...
- If the container is stopped (very likely, given the parent has an end_time and Crashed), it is not consuming CPU. You can safely remove it:
docker rm f3cd5b...
Why this happens
- Exit code -9 is a SIGKILL. In Docker this commonly indicates OOM kill or a hard kill (e.g., docker stop --time 0, host shutdown).
- Because the process was killed abruptly, the Prefect engine did not get to set terminal states for in-flight subflows. Result: subflow stays Running in the API despite no container.
Preventing and auto-recovering “zombie” subflows
- Emit runner heartbeats and add an automation to crash/cancel stalled runs:
- Set in your Docker job env:
PREFECT_RUNNER_HEARTBEAT_FREQUENCY=60
(any integer ≥ 30 enables heartbeats)
- In Automations (UI): create a proactive automation “when a flow-run heartbeat is missing within ~2–3 minutes” → Action: Set state to Crashed (or Cancel). This will catch both top-level and subflows when the process dies.
- Docs: Detect zombie flows, Event triggers
- Add a time-based guardrail as a backup
- Automation: “When a flow run has been Running for > X hours → Set to Crashed (or Cancel)”.
- This sweeps up old zombies even if heartbeats weren’t enabled yet.
- Ensure containers clean up automatically
- In your Docker work pool’s base job template (or per-deployment job_variables), set:
auto_remove: true
- This removes exited containers to avoid clutter.
- Investigate/mitigate -9 kills (probable OOM)
- Check if the container was OOMKilled:
docker inspect -f '{{.State.OOMKilled}}' f3cd5b...
- If true, consider:
- Set memory limits intentionally (so you know when it’s constrained) and adjust the workload:
job_variables = {
"mem_limit": "8g",
"memswap_limit": "8g", # or higher if you allow swap
}
- Reduce concurrency inside the flow (e.g., thread/process pool size) or batch sizes.
- Add retries/timeouts at the flow level:
@flow(timeout_seconds=3600, retries=1, retry_delay_seconds=60)
def my_flow(): ...
What to do with the current stuck subflow
- It has no container attached, so it is not using resources. You can:
- Cancel it:
prefect flow-run cancel b1f749d8-3198-4d0b-add5-d5917412b85f
- Or delete it if you just want to clean the record:
prefect flow-run delete b1f749d8-3198-4d0b-add5-d5917412b85f
Optional extra observabilityMarvin
10/26/2025, 10:07 PMPREFECT_LOGGING_LEVEL=DEBUG prefect worker start -p production-pool
- Worker healthchecks: Worker healthchecks
- States reference: States
If you’d like, share:
- Whether PREFECT_RUNNER_HEARTBEAT_FREQUENCY is currently set for this deployment
- Whether auto_remove: true is configured for your Docker jobs
- The docker inspect OOMKilled output for the parent container
I can then suggest a concrete set of job_variables and an automation recipe tailored to your setup.Olve Heitmann
10/26/2025, 10:42 PMMarvin
10/26/2025, 10:42 PMMarvin
10/26/2025, 10:43 PMimport anyio
from prefect.client.orchestration import get_client
from prefect.states import Cancelled # or Crashed
async def force_cancel(flow_run_id: str):
async with get_client() as client:
await client.set_flow_run_state(flow_run_id, state=Cancelled(), force=True)
anyio.run(force_cancel, "<FLOW_RUN_ID>")
- Replace Cancelled() with Crashed() if you prefer to reflect the abrupt termination.
If you just want to remove the record:
prefect flow-run delete <FLOW_RUN_ID>
Confirming resource usage
- Your subflow shares the parent’s container. From your parent run:
infrastructure_pid was:
<http+docker://localhost:0>f3cd5bd56d434d7192965fab51d060c2e18b5b0f8d31d8a0f8fe0f9acbe9f18
The container ID is the long suffix. Check/cleanup:
docker ps -a --no-trunc --filter id=f3cd5b...
docker inspect -f '{{.State.Status}} Exit={{.State.ExitCode}} OOMKilled={{.State.OOMKilled}}' f3cd5b...
docker logs --tail 200 f3cd5b...
# if stopped, it’s safe to remove
docker rm f3cd5b...
- Since the parent is Crashed and has an end_time, that container is almost certainly stopped already and not using CPU.
To prevent this next time
- Enable runner heartbeats and an automation to mark missed-heartbeat runs Crashed/Cancelled (this will automatically clean up zombies, including subflows whose parent process died):
- Add to your Docker job env:
PREFECT_RUNNER_HEARTBEAT_FREQUENCY=60
- Automation in UI: proactive trigger “when a flow run heartbeat is missing for ~2–3 minutes” → Action: Set state to Crashed (or Cancel).
- Docs: Detect zombie flows
- Consider auto_remove: true in your Docker job to auto-clean exited containers.
If you share the flow run ID that’s stuck in Cancelling, I can give you a concrete force_cancel one-liner you can run.