Taylor Bell
07/06/2026, 7:57 PMasyncio.exceptions.TimeoutError: timed out during opening handshake
Some flow runs fail before any flow code executes, while other runs of the same flow succeed. The crash occurs during runner setup when the FlowRunCancellingObserver tries to open a websocket to the Prefect API server for cancellation events. The websockets library has a hardcoded 10-second open_timeout, and the initial connection attempt in PrefectEventSubscriber.__aenter__ has no retry — a single timeout kills the entire flow run.
We're on Prefect 3.5.0, self-hosted, running on Kubernetes. API server pods are healthy and the API server logs show no connection attempts from the failing jobs, suggesting the connection is lost in the network layer before reaching the server.
Have you seen this before?Marvin
07/06/2026, 7:57 PMMarvin
07/06/2026, 8:01 PMFlowRunCancellingObserver is started as part of runner setup, before user code executes.
- That observer opens a websocket subscription to the Prefect API event stream.
- The initial connect path goes through PrefectEventSubscriber.__aenter__.
- In 3.5.0, that initial connection is effectively a single attempt.
- The underlying websockets client uses an open_timeout of 10 seconds by default.
- Prefect 3.5.0 does not appear to expose a setting to tune that handshake timeout or disable this startup-time websocket requirement.
So if you have an intermittent network-layer issue in Kubernetes — ingress/LB/proxy, DNS, service mesh, NetworkPolicy, node-local egress, idle conn handling, etc. — one bad 10-second handshake can kill the flow run before it starts.
That also matches your observation that:
- some runs succeed,
- some fail before any flow code,
- API logs show no incoming attempt from failed jobs.
That pattern strongly suggests the connection is being dropped or stalled before it reaches the Prefect API process.
This appears improved in later 3.x releases
The codebase has later fixes around this area, including:
- better handling when the observer websocket cannot be established
- retry/backoff behavior for subscriber connection
- improved fallback-to-polling behavior when websocket consumption fails
So on 3.5.0, your diagnosis is plausible and aligns with how startup worked then.
What I’d recommend
1. Upgrade Prefect if you can
- This is the strongest recommendation.
- If you’re on 3.5.0, moving to a newer 3.x should give you better resilience here.
2. Turn on debug logging for a failing run
- Set PREFECT_DEBUG_MODE=1
- That should help surface the full websocket connection error path.
3. Inspect the network path specifically for websocket support/timeouts
Common suspects in self-hosted K8s:
- ingress/proxy not reliably handling websocket upgrade
- service mesh policy/interception
- node/pod DNS slowness
- LB timeout/handshake timeout
- intermittent egress policy issues
- TLS termination/proxy behavior if using wss://
4. Compare successful vs failing pods
Things worth checking:
- node placement
- namespace/network policy
- sidecar injection
- DNS resolver behavior
- whether failures correlate with specific nodes/AZs
5. Try a direct websocket test from inside a job pod
If possible, test connectivity from the same runtime environment to the Prefect events endpoint, not just the REST API endpoint.
Why this is probably not your flow code
Because the failure occurs in runner setup before flow execution, this is infrastructural/client-startup behavior, not an issue in your flow function itself.
Relevant references
- Prefect repo: Prefect on GitHub
- Pricing / versioning info if needed: prefect.io/pricing
If you want, I can help you narrow this down further by looking at:
- your ingress/proxy setup,
- the exact events websocket URL/path being used,
- or the specific Prefect versions where this startup behavior changed.Taylor Bell
07/06/2026, 8:07 PMMarvin
07/06/2026, 8:07 PMMarvin
07/06/2026, 8:07 PMThang Le
07/08/2026, 7:40 PMNate
07/08/2026, 7:54 PMTimeoutError, which on python 3.10 doesn't cover asyncio.exceptions.TimeoutError (the one in the traceback), whereas the 3.6.22 fallback covers it regardless