Prefect 2's biggest adoption cost was the server: running Orion meant standing up a database-backed API service before you could orchestrate anything, which felt heavy for a team that wanted a scheduler, not a platform. Prefect 3 rebuilt the internals around a much lighter self-hosted server and used the opportunity to clean up several rough edges — how work gets picked up, how you react to events instead of only polling on a schedule, and how caching and retries are declared. None of it changes the core mental model: flows and tasks are still the two building blocks, and if you've used Prefect 2 the migration is mostly deletions.
Flows and tasks are still the core abstraction
A @flow is the unit of orchestration — the thing that gets a run ID, shows up in the UI, and can be scheduled or triggered. A @task is a unit of work inside a flow that Prefect tracks individually: its own retries, its own cache key, its own state in the run graph. Nothing about that contract changed in Prefect 3. What changed is that the engine underneath got faster and the transaction semantics around task results got more explicit, which matters once you start relying on caching for anything non-trivial.
from prefect import flow, task
from prefect.tasks import task_input_hash
from datetime import timedelta
@task(retries=3, retry_delay_seconds=10,
cache_key_fn=task_input_hash,
cache_expiration=timedelta(hours=1))
def fetch_report(report_date: str) -> dict:
# network call to an upstream API; cached per report_date
# for an hour so re-running the flow doesn't refetch unchanged data
...
@task
def load_to_warehouse(data: dict) -> None:
...
@flow(name="daily-report-sync", log_prints=True)
def daily_report_sync(report_date: str):
data = fetch_report(report_date)
load_to_warehouse(data)
if __name__ == "__main__":
daily_report_sync(report_date="2026-07-28")
A lighter self-hosted server
Prefect 2's Orion API server was a full database-backed service you ran yourself if you weren't using Prefect Cloud — Postgres or SQLite underneath, a UI, an API, and enough moving parts that a lot of teams defaulted to the hosted option just to avoid operating it. Prefect 3 kept the same self-hosted option but trimmed what it takes to stand up: prefect server start gets you a working server with a bundled SQLite database for local or small-team use, and the deployment model for production no longer assumes you need a dedicated ops team to babysit it. For a solo project or a small pipeline, this is the difference between "orchestration is a side project in itself" and "orchestration is a command you run."
Work pools and workers replace agents
Prefect 2's execution model was built around agents — long-running processes that polled a work queue and executed whatever showed up. Prefect 3 replaced this with work pools and workers, which is a naming change but also a real behavioral one: a work pool describes the infrastructure a deployment should run on (a Docker pool, a Kubernetes pool, a process pool), and a worker is the thing that actually polls that pool and launches runs on the matching infrastructure. The separation makes it clearer which deployments are bound to which execution environment, and it removed a lot of the "why did this run go to the wrong agent" debugging that Prefect 2 users will recognize.
A work pool is created with an infrastructure type — process, Docker, Kubernetes — and every worker that polls it needs to match. If you need flows on two different infrastructures, that's two work pools with two workers, not one pool with mixed workers.
Events and automations for reactive triggers
Prefect 3 leans harder into events: state changes, custom events emitted from within a flow, and external events can all be captured as first-class objects, and automations let you react to them without writing a separate polling process. A flow that should kick off the moment an upstream flow finishes — or the moment three failures happen within an hour — is an automation rule instead of a scheduled job that runs every few minutes just to check. This is the part of Prefect 3 that actually changes how you architect a pipeline, not just how you deploy it: reactive triggers replace a chunk of what used to be cron-plus-polling.
An automation reacting to an event is not instantaneous — there's a small delay while the event is ingested and matched against trigger rules. For anything latency-sensitive, that delay is usually fine; for a hard real-time requirement, an automation is the wrong tool.
Retries, caching, and result persistence are more explicit
Retries and timeouts are still declared as arguments on @task or @flow — retries, retry_delay_seconds, timeout_seconds — but Prefect 3 tightened up how task results get persisted and cached. Result persistence is opt-in and configurable per task, and caching keys off a function you provide (like task_input_hash, which hashes the task's inputs) rather than an implicit assumption about what "the same call" means. That matters in practice: it's the difference between a flow that silently reuses a stale cached result because two calls looked equal to the framework but weren't, and a flow where you decided exactly what equality means.
Wrapping up
Nothing about Prefect 3 asks you to rethink how you write a pipeline — flows and tasks work the same way they did before. What changed is everything around them: a server that's actually reasonable to self-host, work pools and workers that make the execution model legible, and events/automations that let a pipeline react instead of just poll. If your team stalled on Prefect 2 because standing up Orion felt like a project of its own, that specific objection no longer applies.
Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.