Madushan Pathirana
07/21/2026, 4:04 PMMarvin
07/21/2026, 4:04 PMMarvin
07/21/2026, 4:07 PMpython
from prefect import flow, task
from prefect.cache_policies import INPUTS
@task(
persist_result=True,
cache_policy=INPUTS,
)
def build_dataframe(raw_input):
# expensive 30-min processing
df = ...
return df
@task
def downstream_step(df):
# bug-fixing usually happens here
...
@flow
def my_flow(raw_input):
df = build_dataframe(raw_input)
downstream_step(df)
What this does:
- first run: build_dataframe executes normally
- later runs with the same inputs: Prefect reuses the saved result instead of recomputing it
Why INPUTS is a good choice for debugging
By default, Prefect often includes task source/code in cache behavior. For debugging downstream tasks, that may be annoying because editing code can invalidate the cache.
Using cache_policy=INPUTS means:
- if the task inputs are unchanged, reuse the result
- useful when you are not changing the expensive transformation itself
If you do change the expensive task logic and need to recompute it, you can force refresh.
Important caveat
Caching only works reliably when the result is persisted. So make sure result persistence is enabled:
- per task with persist_result=True
- or globally with:
bash
prefect config set PREFECT_RESULTS_PERSIST_BY_DEFAULT=true
Docs:
- Caching concepts
- Result persistence
- Advanced caching
My practical recommendation
For your case, I’d suggest:
1. Put the 30-minute DataFrame step in its own task
2. Enable:
- persist_result=True
- cache_policy=INPUTS
3. Keep the bug-prone downstream work in separate tasks
4. Rerun the flow while debugging; Prefect should skip the expensive step when inputs haven’t changed
When this is a great fit
- source data is stable
- the expensive task is deterministic
- you mostly debug later parts of the flow
When I’d consider something else
If the DataFrame gets very large, serializing it through Prefect results may become a little heavy. In that case, a very common pattern is:
- write the intermediate DataFrame to Parquet
- return the file path / object key from the task
- downstream tasks read that file
That can be better than passing huge DataFrames around in-memory between tasks.
Example pattern:
python
@task(
persist_result=True,
cache_policy=INPUTS,
)
def build_dataframe(raw_input):
df = ...
path = "/tmp/processed_df.parquet"
df.to_parquet(path, index=False)
return path
@task
def downstream_step(df_path):
df = pd.read_parquet(df_path)
...
This is often my preferred production pattern if:
- the intermediate data is large
- you may want to inspect/reuse it outside Prefect
- multiple reruns will use the same materialized dataset
Rule of thumb
- small/medium DataFrame: Prefect result caching is fine
- larger intermediate dataset or production reuse: materialize to Parquet and cache the path/result
Since you mentioned 5000+ rows, row count alone is not too scary — what matters more is:
- number of columns
- data types
- total memory size
So for many 5k-row DataFrames, Prefect caching is completely reasonable.
My recommendation in one line
Start with Prefect task caching + persisted results for that early 30-minute step. If the DataFrame is large enough that serialization becomes annoying, switch to writing the intermediate result to Parquet and passing the path downstream.
If you want, I can show you:
1. a minimal caching example for your current flow, orMarvin
07/21/2026, 4:07 PMMadushan Pathirana
07/21/2026, 4:16 PMMarvin
07/21/2026, 4:17 PMMarvin
07/21/2026, 4:17 PMpython
from prefect import flow, task
from prefect.cache_policies import INPUTS
import pandas as pd
from pathlib import Path
@task(
persist_result=True,
cache_policy=INPUTS,
)
def expensive_transform(raw_path: str) -> str:
df = pd.read_parquet(raw_path)
# 30-min transform
result_df = ...
out_path = f"/tmp/processed_data.parquet"
result_df.to_parquet(out_path, index=False)
return out_path
@task
def debug_step(processed_path: str):
df = pd.read_parquet(processed_path)
...
return df.head()
@flow
def my_flow(raw_path: str):
processed_path = expensive_transform(raw_path)
debug_step(processed_path)
Why this is better for large DataFrames
- avoids serializing a huge pandas object into Prefect’s result store
- Parquet is usually much more efficient than pickling DataFrames
- easier to inspect manually
- easier to reuse outside Prefect
- better if you later move to remote storage like S3
Even better if you run on EC2 / server
Since you’re running on a single machine, you could checkpoint to:
- local disk for quick debugging
- or S3 if you want durability across instance restarts/redeploys
Examples:
- local: /data/checkpoints/processed_20250721.parquet
- S3: <s3://my-bucket/prefect-checkpoints/processed_20250721.parquet>
Recommended decision
- Large DF: save to Parquet, return path
- Medium DF: Prefect task caching may still be okay
- Need cross-run reuse and inspectability: Parquet/object storage is usually best
One more important point
If you use a fixed filename like /tmp/processed_data.parquet, later runs may overwrite it. Better to make the path deterministic from the input/version, for example:
python
import hashlib
def make_key(source_name: str) -> str:
return hashlib.md5(source_name.encode()).hexdigest()
@task
def expensive_transform(raw_path: str) -> str:
key = make_key(raw_path)
out_path = f"/tmp/checkpoints/{key}.parquet"
...
return out_path
That gives you a reusable checkpoint per input.
My practical recommendation for you
For a pandas-heavy 30-minute step, if the DF is “too large”, use Prefect to orchestrate the step, but let Parquet be the cache.
So:
- Prefect task = compute/checkpoint management
- Parquet file = actual reusable intermediate artifact
If you want, I can give you a full Prefect 3 example with:
- cache-if-file-exists logic
- local disk version
- S3 version