<@ULVA73B9P> We have some flows which calls some Prefect Tasks. Those Tasks take as parameters a ver...
r
@Marvin We have some flows which calls some Prefect Tasks. Those Tasks take as parameters a very long list of big Pydantic models. Those tasks are really fast to run, but there seems to have a big prefect overhead before those go in Running State. We already tried using quote to speed things up which improved things up and the PREFECT_TASKS_DEFAULT_NO_CACHE=true is already set. Can you help us find out every thing prefect might do before a Task start and what might slow things down with big parameters inputs.
m
thought for 208.1 seconds
Yes — with large nested Pydantic inputs, there are a few places in Prefect 3.x where you can pay noticeable overhead before a task reaches
Running
. 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.
Even if serialization is not happening for results yet, inspection/traversal still is. ### C. Embedded resolution checks Even if there are no futures in the models, Prefect still has to check. ### D.
.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:
Copy code
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:
Copy code
@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:
Copy code
result = my_task(big_models)
try:
Copy code
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:
Copy code
@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:
Copy code
@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 B
- quote all params That usually reveals which parameter still causes the walk. --- ## 10. Consider
opaque(...)
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
Copy code
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:
Copy code
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.