rwitz
06/30/2026, 1:11 PMMarvin
06/30/2026, 1:11 PMMarvin
06/30/2026, 1:15 PMRunning.
The biggest takeaway is:
Prefect inspects task parameters more than once before execution, and for large nested objects that can be expensive even if the task body itself is very fast.
Here’s the pre-Running path and what can slow it down.
---
## What Prefect does before a task starts running
When you call a task inside a flow, Prefect generally does this:
1. Collect call parameters
2. Create a local TaskRun object
3. Inspect parameters to discover upstream dependencies
4. Resolve futures/states inside parameters
5. Wait on explicit wait_for dependencies
6. Compute cache key / transaction key if caching is enabled
7. Acquire concurrency/tag leases if applicable
8. Transition to Running
For big Pydantic models / long nested lists, the main costs are usually steps 3 and 4.
---
## The main overhead with large parameters
### 1. Parameter traversal for dependency discovery
Before the task runs, Prefect walks the parameter values to see whether they contain:
- `PrefectFuture`s
- State objects
- values that came from upstream task results
This is used for dependency tracking and orchestration metadata.
If your parameter is something like:
- a large list of Pydantic models
- a deeply nested model
- a big dict/list tree
Prefect may recursively traverse the whole thing.
That traversal can be expensive even if there are no futures inside.
### 2. Parameter traversal again for resolution
Prefect also resolves any futures/states embedded in parameters into final Python values before task execution.
That means another recursive walk over the same large structure.
So for large inputs, you can effectively pay for multiple passes over the data before user code starts.
### 3. Cache key computation
If caching is enabled, Prefect may compute a cache/transaction key based on task inputs.
For large inputs, hashing/serializing enough of those inputs to build the key can add overhead.
You mentioned PREFECT_TASKS_DEFAULT_NO_CACHE=true is already set — that helps by disabling one source of overhead.
### 4. Concurrency limits / tag leases
If the task has tags associated with concurrency limits, Prefect may wait to acquire a lease before starting.
This is usually not related to large Pydantic inputs, but it can be another reason a task sits before Running.
### 5. Custom task run naming
If you use a dynamic task run name function/template that inspects large parameters, that can also add a bit of overhead.
---
## Why quote(...) helps
quote(...) tells Prefect not to introspect/traverse that value.
That means it can skip a lot of the expensive recursive inspection work for that parameter.
This is why you saw improvement already.
Important caveat:
- quote(...) also disables Prefect’s dependency tracking inside that value
- if a quoted object contains futures/states, Prefect won’t resolve/track them as usual
So quote(...) is best when the large input is just plain data and not carrying embedded Prefect futures/states.
Docs/source-relevant concept:
- quote and opaque annotations are in Prefect’s annotations utilities
- quote is the stronger “don’t inspect this” option
---
## Things that most likely slow your case down
Given your description:
- tasks are very fast
- parameters are very large
- delay is before Running
- quote helped
- caching is already disabled
The most likely remaining overhead is:
### A. Recursive traversal of non-quoted parameters
If only some parameters are quoted, Prefect will still walk the others.
Even one large unquoted field can dominate startup latency.
### B. Large lists of Pydantic models
Pydantic models are Python objects with nested fields; walking a long list of them can be costly.Marvin
06/30/2026, 1:15 PM.submit() vs direct task call
If you are calling tasks synchronously as my_task(...), the caller pays the startup cost inline.
If you use my_task.submit(...), some of that work shifts into the task runner/executor path, which can make the flow feel more responsive.
---
## Practical things to check and try
## 1. Quote every large data parameter
If a task accepts multiple large arguments, quote all of them, not just one.
Example:
from prefect import flow, task
from prefect.utilities.annotations import quote
@task
def process(models, config, metadata):
return len(models)
@flow
def my_flow(models, config, metadata):
process(
quote(models),
quote(config),
quote(metadata),
)
If only models is quoted but config is still a huge nested model, Prefect still traverses config.
---
## 2. Prefer smaller task inputs when possible
Instead of passing a huge list of big models into many fast tasks, pass:
- IDs
- file paths
- object store keys
- database row keys
- lightweight summaries
Then load inside the task if needed.
Example pattern:
@task
def process_batch(batch_path: str):
models = load_models(batch_path)
return do_fast_work(models)
This is often the biggest architectural win.
---
## 3. Convert Pydantic models to a cheaper representation only if it actually helps
Sometimes Pydantic objects are more expensive to traverse than a flatter representation; sometimes converting them first just adds more cost.
So test both:
- list of Pydantic models
- quoted list of Pydantic models
- quoted lightweight dicts / tuples / IDs
In many cases, quoted original objects is already best.
---
## 4. Use .submit() if synchronous startup latency is hurting flow throughput
If you’re currently doing:
result = my_task(big_models)
try:
future = my_task.submit(big_models)
result = future.result()
This can help because work is handed off to the task runner instead of blocking the flow thread as much up front.
It may not eliminate the total overhead, but it can improve perceived latency and parallelism behavior.
---
## 5. Check whether wait_for is involved
If you pass wait_for=[...], Prefect will resolve and wait on those dependencies before entering Running.
If your delay is only for tasks with wait_for, that is another source.
---
## 6. Check task tags / concurrency limits
If the task has tags that map to concurrency limits, it may be waiting for a slot.
That delay can look like “not entering Running yet” even though parameter handling is not the main problem.
If relevant, inspect:
- task tags
- global/tag concurrency limits
- work pool / worker concurrency settings
---
## 7. Make sure dynamic task run names are not inspecting huge inputs
If you have something like:
@task(task_run_name=lambda: ...)
or templated names using large parameters, that may add overhead too.
Keep task names simple and avoid formatting from big models.
---
## 8. Keep results/futures out of giant nested structures
If upstream task futures/results are embedded somewhere inside a giant object graph, Prefect has to search the graph to find and resolve them.
A much better pattern is:
- keep Prefect futures as top-level task arguments
- keep large payload data separate and quoted
Example:
@task
def upstream():
return "small-key"
@task
def downstream(key, payload):
...
@flow
def f(payload):
key_future = upstream.submit()
downstream.submit(key_future, quote(payload))
This is better than hiding key_future deep inside payload.
---
## 9. Benchmark with and without quote per parameter
A simple way to isolate the culprit is to test combinations:
- no quoting
- quote only param A
- quote only param BMarvin
06/30/2026, 1:15 PMopaque(...) in some future-heavy cases
If you need Prefect to resolve a top-level future but want to avoid recursively traversing the resolved large value, opaque(...) can help.
Use this when the input is an upstream future whose result is large.
quote(...) is usually the better fit for plain large data you already have locally.
---
## What is not likely the main issue here
Since you said the delay is before Running, these are less likely to be the primary cause:
- result persistence after the task body
- task result serialization on completion
- storage block writes after execution
Those matter later, but not for the pre-Running delay you described.
---
## Recommended strategy for your case
I’d try this order:
1. Quote every large parameter
2. Ensure no large nested unquoted fields remain
3. Keep futures/states out of those large structures
4. Switch fast tasks to .submit() where possible
5. Pass references/keys instead of full model lists where practical
6. Check concurrency/tag limits if delays still remain
---
## Example pattern I’d recommend
from prefect import flow, task
from prefect.utilities.annotations import quote
@task
def fast_task(model_batch, shared_config):
return len(model_batch)
@flow
def my_flow(model_batch, shared_config):
future = fast_task.submit(
quote(model_batch),
quote(shared_config),
)
return future.result()
And if possible, even better:
from prefect import flow, task
@task
def fast_task(batch_uri: str, config_uri: str):
model_batch = load_batch(batch_uri)
shared_config = load_config(config_uri)
return len(model_batch)
@flow
def my_flow(batch_uri, config_uri):
return fast_task.submit(batch_uri, config_uri).result()
---
## One important nuance
PREFECT_TASKS_DEFAULT_NO_CACHE=true helps only with cache-key related overhead. It does not stop Prefect from:
- traversing parameters for dependency discovery
- resolving futures/states in parameters
- waiting on dependencies
- acquiring concurrency leases
So it’s helpful, but it won’t solve the biggest issue when the payload itself is huge.
---
## Relevant links
- Prefect tasks docs: Tasks
- Prefect flows docs: Flows
- Prefect repo: Prefect source
- prefect deploy / Prefect 3 docs home: Prefect 3 documentation
---
If you want, I can help with a concrete profiling checklist for your flows, or help rewrite one of your task signatures to minimize this startup overhead.Bring your towel and join one of the fastest growing data communities. Welcome to our second-generation open source orchestration platform, a completely rethought approach to dataflow automation.
Powered by