Ihor Ramskyi
06/09/2026, 2:12 PMMarvin
06/09/2026, 2:13 PMMarvin
06/09/2026, 2:19 PMCached instead of re-executing.
A few key points:
### Why this happens
By default, Prefect tasks may use an implicit cache policy equivalent to hashing things like:
- task inputs
- task source
- run context / run id
If Prefect finds an existing persisted result for that cache key, it can reuse it and mark the task run as Cached.
### Important detail: caching depends on persisted results
A task can only really be reused if its result was persisted somewhere. Prefect can automatically enable result persistence when caching-related settings are in play, even if you didn’t manually configure a cache policy.
So the behavior is often:
1. task gets a default cache policy
2. result persistence is enabled
3. a later matching run finds the stored result
4. task enters Cached
### Common reasons users see this unexpectedly
- You’re using plain @task and assuming “no cache policy” means “no caching”
- A prior run already produced a persisted result for the same task/input combination
- Global settings are enabling caching or result persistence
- Task workers / orchestration behavior are deduplicating matching task runs
### How to turn it off explicitly
If you never want a task to cache, set `NO_CACHE`:
from prefect import task
from prefect.cache_policies import NO_CACHE
@task(cache_policy=NO_CACHE, persist_result=False)
def my_task(x):
return x * 2
If you want to inspect what Prefect thinks your task is configured with:
print(my_task.cache_policy)
print(my_task.persist_result)
### Things to check in your environment
Look for settings like:
- PREFECT_TASKS_DEFAULT_NO_CACHE
- PREFECT_TASKS_DISABLE_CACHING
- PREFECT_RESULTS_PERSIST_BY_DEFAULT
These can change the default behavior across your environment.
### Docs
- Caching docs
- Result persistence docs
- Cache policies API
If you want, I can also help you diagnose a specific task and show exactly why it was marked Cached from its config/code.Ihor Ramskyi
06/09/2026, 2:23 PMMarvin
06/09/2026, 2:23 PMMarvin
06/09/2026, 2:26 PMCached across your workspace.
Important nuance:
- Cached is typically a state name
- the underlying state type is usually still COMPLETED
So you want to filter by state name = Cached, not just by completed runs.
### Easiest: CLI
Try:
prefect task-run ls --state Cached --limit 100
If you want machine-readable output:
prefect task-run ls --state Cached --output json
Then inspect any suspicious one:
prefect task-run inspect <TASK_RUN_ID>
I tried to verify the exact CLI help, but the command help timed out in my environment, so if your local CLI version differs slightly, run:
prefect task-run --help
prefect task-run ls --help
### Python client: check across all flow runs
This is more reliable if you want a complete audit:
from prefect.client.orchestration import get_client
from prefect.client.schemas.filters import (
TaskRunFilter,
TaskRunFilterState,
TaskRunFilterStateName,
)
from prefect.client.schemas.sorting import TaskRunSort
import asyncio
async def find_cached_task_runs(limit=200):
task_run_filter = TaskRunFilter(
state=TaskRunFilterState(
name=TaskRunFilterStateName(any_=["Cached"])
)
)
async with get_client() as client:
runs = await client.read_task_runs(
task_run_filter=task_run_filter,
limit=limit,
sort=TaskRunSort.EXPECTED_START_TIME_DESC,
)
for tr in runs:
state = tr.state
details = state.state_details if state else None
print(f"task_run_id={tr.id}")
print(f"task_name={tr.name}")
print(f"flow_run_id={tr.flow_run_id}")
print(f"state_name={state.name if state else None}")
print(f"state_type={state.type if state else None}")
print(f"cache_key={getattr(details, 'cache_key', None)}")
print(f"cache_expiration={getattr(details, 'cache_expiration', None)}")
print("-" * 60)
asyncio.run(find_cached_task_runs())
### Narrow it to one flow run
from prefect.client.orchestration import get_client
from prefect.client.schemas.filters import (
TaskRunFilter,
TaskRunFilterState,
TaskRunFilterStateName,
TaskRunFilterFlowRunId,
)
import asyncio
async def find_cached_for_flow_run(flow_run_id):
task_run_filter = TaskRunFilter(
state=TaskRunFilterState(
name=TaskRunFilterStateName(any_=["Cached"])
),
flow_run_id=TaskRunFilterFlowRunId(any_=[flow_run_id]),
)
async with get_client() as client:
runs = await client.read_task_runs(task_run_filter=task_run_filter)
return runs
### What to look for
For each cached task run, inspect:
- flow_run_id
- state.name → should be Cached
- state.state_details.cache_key
- state.state_details.cache_expiration
If you see the same cache_key appearing repeatedly across different flow runs, that’s a strong sign Prefect has been reusing prior results.
### In the UI
You can also check in Prefect UI by filtering task runs by state Cached. That uses the same underlying API filter.
### Docs
- Caching docs
- Result persistence docs
If you want, I can also give you a small script that summarizes:
- which tasks were cached most often
- which flow runs they appeared in
- which cache keys were reused across runsIhor Ramskyi
06/09/2026, 3:12 PMMarvin
06/09/2026, 3:12 PMMarvin
06/09/2026, 3:18 PMCached even if this is the first task run you’re looking at in the UI.
That prior cached result could have come from:
- an earlier flow run
- an earlier run of the same code with the same inputs
- a previous deployment / renamed task / recreated task definition
- another environment sharing the same result storage
#### 2. The task was called more than once with identical inputs in the same flow
If the task is invoked twice in one flow run with the same effective cache key, the second invocation can show up as Cached.
#### 3. A concurrent run “won the race”
If two matching task executions started around the same time, one may complete and persist the result first, and the other can then reuse it and show Cached.
#### 4. A previous persisted result existed even if the prior run record is gone or not obvious
Sometimes the persisted artifact/result exists even when the older run is not easy to find anymore, for example due to:
- different workspace visibility/context
- old run history being harder to locate
- result storage surviving longer than what you’re viewing in the UI
### Why this is possible technically
In Prefect 3, the engine/orchestration checks for an existing result for the computed cache key before actually executing the task body.
If it finds one, it can:
- skip execution
- read the result
- set the state name to Cached
So Cached does not mean “this task run itself executed before”.
It means “Prefect found a reusable result for this cache key”.
### How to debug this
Inspect the cached task run and compare:
- task_run.state.state_details.cache_key
- task_run.flow_run_id
- task inputs
- task source/code version
- result storage location
You want to find other task runs with the same cache key.
Here’s a useful script:
from prefect.client.orchestration import get_client
from prefect.client.schemas.filters import TaskRunFilter
import asyncio
async def inspect_task_run(task_run_id):
async with get_client() as client:
tr = await client.read_task_run(task_run_id)
print(f"task run: {tr.id}")
print(f"name: {tr.name}")
print(f"flow_run_id: {tr.flow_run_id}")
print(f"state: {tr.state.name} / {tr.state.type}")
details = tr.state.state_details
cache_key = getattr(details, "cache_key", None)
print(f"cache_key: {cache_key}")
print(f"cache_expiration: {getattr(details, 'cache_expiration', None)}")
asyncio.run(inspect_task_run(\"YOUR_TASK_RUN_ID\"))
If you can access your API/database more broadly, the ideal audit is:
- get the cache_key from the cached run
- search for other task runs sharing that key
- check whether another run completed earlier
### How to force fresh execution temporarily
If you want to test whether cache reuse is the cause:
from prefect import task
from prefect.cache_policies import NO_CACHE
@task(cache_policy=NO_CACHE, persist_result=False)
def my_task(x):
return x * 2
Or use cache refresh if that fits your setup.
### Docs
- Caching docs
- Result persistence docs
If you want, I can help you write a script that:
1. takes one cached task run ID
2. extracts its cache_key
3. searches your recent task runs for probable source runs that produced that cached result.Ihor Ramskyi
06/10/2026, 11:42 AMtasks: {
"refresh_cache": false,
"default_no_cache": false,Marvin
06/10/2026, 11:42 AMMarvin
06/10/2026, 11:42 AMtasks.refresh_cache: false
This means Prefect is allowed to reuse existing cached results.
- false = use cache if a matching cache key exists
- true = bypass existing cache and force fresh execution
So this setting absolutely affects why you may see Cached.
#### tasks.default_no_cache: false
This means Prefect does not disable default caching behavior for tasks.
In practice, this allows Prefect’s default task cache policy behavior to remain active.
- false = default caching is still allowed
- true = tasks default to no caching unless you explicitly enable it
So this is also directly related.
### In your case
With:
tasks: {
"refresh_cache": false,
"default_no_cache": false,
}
you are basically telling Prefect:
- do not force recomputation
- do not disable task caching by default
That is consistent with tasks unexpectedly entering Cached.
### If you want to stop this behavior globally
You’d generally want:
tasks: {
"refresh_cache": true,
"default_no_cache": true,
}
But note the difference:
- refresh_cache: true = ignore existing cache and recompute
- default_no_cache: true = don’t apply default caching behavior to tasks in the first place
Usually, if your goal is “I never want implicit caching”, default_no_cache: true is the more important one.
### Docs
- Caching docs
- Result persistence docs
If you want, I can help map those YAML-style settings to the exact environment variables / Python settings names too.