Kyle McChesney
08/05/2026, 1:51 PMMarvin
08/05/2026, 1:51 PMMarvin
08/05/2026, 1:53 PM.submit() / mapping
- Observability in the UI/logs/state history
- Better isolation for debugging and failure handling
What the overhead is
Prefect docs do acknowledge per-task overhead, but they do not give a universal fixed number. The overhead comes from things like:
- creating/tracking task runs
- state transitions
- logging / event emission
- task-runner scheduling / futures management
So the practical question is less “what is the exact overhead?” and more:
Is this step important enough to justify orchestration?
Useful mental model
Make it a task if the step is:
- expensive
- flaky
- worth retrying independently
- worth caching
- worth running in parallel
- useful to see separately in the UI
Keep it inline / inside one task if the step is:
- very small and fast
- tightly coupled to adjacent code
- not useful to retry or observe separately
- one of many tiny operations where task orchestration would dominate runtime
Examples
Good task boundaries:
- extract from API
- clean dataset
- write to warehouse
- score one partition / file / batch
- call an unreliable external service
Probably too fine-grained:
- a few lines of lightweight transformation
- individual arithmetic / field munging steps
- hundreds/thousands of sub-millisecond operations
A nice heuristic:
- If you’d care about this step having its own retry/caching/logging/state, make it a task.
- If not, keep it as normal code.
Also, Prefect docs explicitly note that sometimes it’s better to parallelize inside a single task with your own thread/process pool if you’re trying to avoid per-task overhead for lots of tiny units of work.
So a common pattern is:
python
from prefect import flow, task
@task(retries=3)
def fetch_data():
...
@task
def process_batch(batch):
# several lightweight steps can stay inline here
cleaned = ...
enriched = ...
validated = ...
return validated
@task
def write_results(results):
...
@flow
def pipeline():
data = fetch_data()
processed = process_batch(data)
write_results(processed)
Instead of turning every tiny transform into its own task.
Practical guidance I’d use
- Start with logical business steps as tasks
- Don’t break apart tiny in-memory transforms unless you need orchestration features
- If you have many very small units, batch them into one task
- If performance matters, benchmark your actual workflow shape rather than assuming finer granularity is always better
Relevant docs:
- Tasks concept docs
- Flows concept docs
- Task runners docs
- Results and caching docs
- Logging docs
If you want, I can also turn this into a more concrete decision checklist like “when to make something a task in ETL/ML/dataframe-heavy workflows.”