Janet Carson
06/01/2026, 4:26 PMMarvin
06/01/2026, 4:26 PMJanet Carson
06/01/2026, 4:43 PMMarvin
06/01/2026, 4:43 PMJanet Carson
06/01/2026, 4:46 PMMarvin
06/01/2026, 4:56 PMMarvin
06/01/2026, 5:00 PMtext
Worker channel unhealthy, REST fallback is active: received 1013 ... service_unavailable
Exiting worker context...
Tearing down worker...
Stopping runner...
...
After that, a healthy worker should either:
1. continue polling via REST, or
2. fully exit so your supervisor restarts it
Instead, yours appears to get stuck after teardown starts, while the process stays alive and spins CPU. That suggests the worker is hanging during shutdown/cleanup rather than returning to normal polling.
My best read: this is likely a bug in 3.7.2 around worker-channel/websocket failure handling and teardown.
A few useful points:
- 1013 service_unavailable is a websocket-side “try again later” signal
- Prefect logs that REST fallback is active, but then immediately logs Exiting worker context...
- your later logs are mostly from the events client reconnecting, not from the worker’s flow-run polling loop
- since you never see Querying for flow runs... again, the main polling loop likely stopped
I wasn’t able to confirm an exact GitHub issue because GitHub search failed from my side, but the source-code research strongly suggests this is in the worker/channel lifecycle rather than your deployment config.
What I’d do first
1. Upgrade off 3.7.2 if possible
- Even if there isn’t a confirmed one-line fix in 3.7.3 specifically, I would not stay on 3.7.2 for this.
- If you can reproduce on the latest 3.x, that’s much more actionable.
- Release notes: Prefect releases
2. Run the worker with debug logging
Set:
bash
PREFECT_LOGGING_LEVEL=DEBUG
PREFECT_DEBUG_MODE=1
Then capture the full sequence from startup through the “Exiting worker context...” line.
3. Get a Python stack dump from the stuck process
This is the fastest way to tell what loop is spinning.
Start the worker with:
bash
PYTHONFAULTHANDLER=1
Then when it gets stuck, send:
bash
kill -USR1 <pid>
or sometimes:
bash
kill -USR2 <pid>
depending on your environment / handler setup.
Also, if you can attach with py-spy, that’s even better:
bash
py-spy dump --pid <pid>
py-spy top --pid <pid>
4. Check whether it’s spinning in websocket/events code
Your logs already suggest that at least part of the remaining activity is in prefect.events.clients. If py-spy top shows time concentrated there, that’s a strong clue.
5. Use an external supervisor
Until root cause is resolved, make sure the worker is restarted if it stops making progress.
- systemd Restart=always
- Docker/Kubernetes restart policy
- liveness check based on recent polling / heartbeat logs
Why this looks buggy
Based on code-path research, the likely failure mode is:
- worker websocket becomes unhealthy
- worker begins teardown / exits its context
- a background task tied to the worker channel or events client keeps running/retrying
- the main polling loop never resumes
- process remains alive, so your service manager doesn’t necessarily restart it
- CPU rises because it’s stuck in a retry/spin loop
Concrete debugging checklist
Please collect these from one stuck instance:
bash
prefect version
python --version
pip show prefect
env | grep PREFECT
Then while stuck:
bash
ps -fp <pid>
top -Hp <pid>
py-spy dump --pid <pid>
py-spy top --pid <pid>
strace -p <pid>
If you’re in containers:
bash
kubectl logs <pod> --previous=false
kubectl describe pod <pod>
Things to look for specifically
- Is there a proxy / load balancer in front of the Prefect API that may be interrupting websockets?
- Does this happen only against self-hosted server, or also against Cloud?
- Are there ingress timeouts, idle timeouts, or websocket upgrade restrictions?Marvin
06/01/2026, 5:00 PM1013 service_unavailable, I’d also inspect anything between worker and API:
- nginx / ALB / ingress websocket support
- idle timeout settings
- reverse proxy buffering / upgrade headers
- server overload around the time of failure
If you’re self-hosting Prefect Server behind ingress, a websocket-unfriendly proxy could absolutely be part of this.
Recommended next step
If you want, send me:
- how you start the worker
- whether you use Prefect Cloud or self-hosted server
- whether there is nginx / ALB / ingress in front of the API
- a py-spy dump or faulthandler stack from the stuck process
and I can help narrow down whether this is:
1. worker bug,
2. events-client bug, or
3. websocket/proxy infrastructure causing the bad state.Janet Carson
06/01/2026, 5:06 PMMarvin
06/01/2026, 5:07 PMMarvin
06/01/2026, 5:07 PMiptables / firewall rules, block traffic for ~30–60 seconds, then remove the rule.
If you don’t want to mess with firewall rules, an easier version is:
- stop outbound network from that VM/container temporarily
- or kill the route to the internet briefly
- or use a proxy in front of the worker and bounce the proxy
2. Blackhole DNS for the Prefect API host
If the worker resolves the Cloud endpoint through local DNS, temporarily override resolution to a dead IP, then restore it.
3. Use tc netem to inject network faults
This is often the cleanest reproducible test:
- add packet loss
- add high latency
- add connection resets
Example idea:
bash
tc qdisc add dev eth0 root netem loss 30% delay 1000ms 500ms
# let it run briefly
tc qdisc del dev eth0 root
4. Send `SIGSTOP`/`SIGCONT` to the process
Less realistic, but sometimes enough to trigger heartbeat / websocket unhealthy behavior:
bash
kill -STOP <pid>
sleep 60
kill -CONT <pid>
Best reproduction approach
If your goal is “get to failure fast and inspect it,” I’d do this:
1. run one dedicated worker with:
bash
PREFECT_LOGGING_LEVEL=DEBUG
PREFECT_DEBUG_MODE=1
PYTHONFAULTHANDLER=1
prefect worker start --with-healthcheck --pool pool-name
2. induce a short network failure for 30–120s
3. when it reaches the stuck state, immediately collect:
bash
py-spy top --pid <pid>
py-spy dump --pid <pid>
That should tell us whether it’s spinning in:
- worker channel code
- events client websocket code
- healthcheck server loop
- some AnyIO cancellation edge case
Given your new detail, here’s my updated hypothesis
Because:
- worker is local
- backend is Prefect Cloud
- websocket unstable/fallback messages are continuous
this now looks even more like a websocket/channel problem than a scheduling problem.
In other words:
- the worker may be surviving on REST fallback for a long time
- eventually one of those repeated websocket/channel failures puts it into the bad teardown state
- after that it stops polling altogether
So the repeated unhealthy websocket messages are probably the precursor, not just noise.
Two things I’d specifically check
1. Does the host have anything interfering with long-lived outbound websocket connections?
Examples:
- corporate proxy
- egress filtering
- TLS inspection
- unstable NAT/firewall idle handling
- VPN
2. Does systemd think the service is healthy after this happens?
Since you’re using --with-healthcheck, if the process is stuck but the healthcheck still passes, that’s important.
Check whether the health endpoint still returns healthy after the worker stops polling.
Practical debugging with systemd
When it fails, capture:
bash
systemctl status <service-name>
journalctl -u <service-name> -n 300 --no-pager
curl <http://127.0.0.1:<healthcheck-port>/health>
If the healthcheck still says healthy while the worker is no longer polling, that’s a useful signal.
If you want the quickest minimal repro
The simplest likely-to-work test is:
bash
# start worker with debug logging
PREFECT_LOGGING_LEVEL=DEBUG PREFECT_DEBUG_MODE=1 prefect worker start --with-healthcheck --pool pool-name
Then from another shell, interrupt network for ~1 minute:
bash
kill -STOP <pid>
sleep 60
kill -CONT <pid>Marvin
06/01/2026, 5:07 PMtc or firewall rules.
If you want, I can give you:
- a safe tc netem recipe to simulate Cloud websocket instability
- a systemd unit override that auto-restarts on this stuck condition
- or a step-by-step capture plan for the next repro with py-spy and healthcheck output