Worker Lifecycle and Resilience#
Prefect workers run as long-lived processes that poll a work pool for scheduled flow runs. The lifecycle has three distinct phases: CLI pre-flight validation, setup/backend sync, and the steady-state service loop. All three phases are implemented across src/prefect/cli/worker.py, src/prefect/cli/_worker_utils.py, and src/prefect/workers/base.py.
Phase 1: CLI Pre-flight Validation#
Before a worker object is ever instantiated, prefect worker start performs several gate-keeping checks:
-
Pause state checks β
_check_work_pool_pausedand_check_work_queues_pausedquery the API and emit a warning if the pool or queues are paused. The worker still starts, but the user is warned it won't execute runs until unpaused . -
Worker type resolution β If
--typeis not supplied,_retrieve_worker_type_from_poolreads the work pool from the API and discovers the type. If the pool doesn't exist, it defaults to"process". Push/managed pools are rejected here with a hard exit . -
Package install β
_load_worker_classattempts to resolve the worker type via the plugin registry. If that fails,_find_package_for_worker_typequeries the Prefect collections metadata API to find the right integration package. Depending on--install-policy(always,if-not-present,prompt,never), the package is installed via_install_packagebefore proceeding . -
Signal handlers β
setup_signal_handlers_workeris registered so SIGINT/SIGTERM produce clean shutdowns rather than tracebacks .
Phase 2: Setup and Backend Sync#
BaseWorker.start() calls async with self as worker, which invokes setup():
- Validates that
PREFECT_API_URLis set . - Creates the
PrefectClientand capacity limiter, and seedsos.environ["PREFECT__WORKER_NAME"]for attribution headers . - Calls
sync_with_backend()immediately to register the worker and obtain itsbackend_idbefore the main loop starts. - Optionally starts a
FlowRunCancellingObserverwhenworker.enable_cancellationis true.
Worker Channel (sync_with_backend)#
sync_with_backend delegates to a WorkPoolWorkerChannel, introduced in PR #21885. The channel uses a WebSocket-first, REST-fallback strategy:
- Primary: a persistent WebSocket connection delivers work-pool snapshots, heartbeats, and configuration updates in real time.
- Fallback: when the WebSocket endpoint is unavailable, the channel falls back to REST heartbeat calls.
PR #21919 hardened the REST fallback against a race condition where two workers start simultaneously: if create_work_pool raises ObjectAlreadyExists, the worker now re-reads the pool created by the other worker instead of crashing.
_record_worker_id captures the backend-assigned UUID into self.backend_id and sets os.environ["PREFECT__WORKER_ID"] . _record_work_pool_snapshot refreshes the local copy of the work pool, warning if the remote type doesn't match the local worker type .
Phase 3: The Service Loop#
BaseWorker.start() spins up two concurrent critical_service_loop tasks inside an anyio task group:
| Loop | Workload | Interval setting |
|---|---|---|
| Poll loop | get_and_submit_flow_runs | PREFECT_WORKER_QUERY_SECONDS |
| Sync loop | sync_with_backend | PREFECT_WORKER_HEARTBEAT_SECONDS |
Both loops use backoff=4 (up to ~4 doublings, ~1 minute max interval) and jitter_range=0.3 .
critical_service_loop Resilience#
critical_service_loop wraps any workload with structured retry/backoff logic:
- Transient errors tolerated:
httpx.TransportError(connection/timeout) and HTTP 5xx responses are caught and recorded without crashing . - Consecutive-error tracking: a fixed-size deque of
consecutive(default 3) booleans tracks the most recent outcomes. If all entries areFalse, backoff is triggered . - Exponential backoff: on each backoff event, the sleep interval becomes
interval Γ 2^backoff_count. Workers call these loops withbackoff=4, so they survive up to 4 consecutive-failure cycles before raisingRuntimeError("Service exceeded error threshold."). - Reset on success: any successful workload invocation resets
backoff_countto 0 . - Non-transient errors: anything other than
TransportErroror 5xx immediately re-raises, exiting the loop .
Healthcheck#
When started with --with-healthcheck, a uvicorn ASGI server runs on a daemon thread. is_worker_still_polling() returns False if _last_polled_time is more than query_interval_seconds Γ 30 seconds old, allowing an external orchestrator (e.g., Kubernetes liveness probe) to restart a stuck worker.
Teardown#
teardown() cancels all scheduled task scopes, stops the worker channel, emits a prefect.worker.stopped event, and closes the client. Environment variables PREFECT__WORKER_NAME and PREFECT__WORKER_ID are cleaned up only if they belong to this worker instance (guarding against multi-worker-per-process scenarios).
Key Settings#
| Setting | Default | Purpose |
|---|---|---|
PREFECT_WORKER_QUERY_SECONDS | 10s | Poll loop interval |
PREFECT_WORKER_HEARTBEAT_SECONDS | 30s | Sync/heartbeat interval |
PREFECT_WORKER_PREFETCH_SECONDS | 10s | How far ahead to fetch scheduled runs |
Related Files#
src/prefect/workers/base.pyβBaseWorkerimplementationsrc/prefect/cli/worker.pyβprefect worker startcommandsrc/prefect/cli/_worker_utils.pyβ pre-flight helperssrc/prefect/utilities/services.pyβcritical_service_loop- PR #21885 β worker channel / WebSocket sync
- PR #21919 β concurrent work-pool creation fix