When Your Streamlit App Reloads Everything: A Practical Guide to st.cache_data
Streamlit reruns your whole script on every interaction. Here's how st.cache_data fixes slow data loads, how its invalidation works, and the memory and staleness trade-offs to manage.
13 Aug 2026, 13:10 UTC

The problem: every click re-runs your data pipeline
Streamlit's execution model is simple: when a user interacts with any widget, the whole script runs again from top to bottom. That's what makes apps easy to write — and it's also why a dashboard that loads a 500 MB CSV or calls a slow API feels broken. Every slider move triggers the same expensive load, and the user stares at a spinner for work that already happened.
The useful takeaway: st.cache_data lets you mark the expensive parts so they run once and return instantly afterward, keyed by their inputs. Used well, it turns an unusable app into a snappy one with a few lines of code. Used carelessly, it eats your RAM and serves stale data. This post covers how to tell the difference.
How st.cache_data decides to re-run
Decorating a function with @st.cache_data tells Streamlit to hash two things: the function's source code and the arguments it was called with. If both match a previous call, Streamlit skips execution and returns the stored result. If either changes — you edit the function body, or a user picks a different filter value — the cache misses and the function runs again.
Two details matter in practice:
- Arguments must be hashable. Strings, numbers, and tuples are fine. Passing an open database connection or an unhashable custom object raises a hashing error. For resources like connections, Streamlit provides a separate decorator,
st.cache_resource, which is designed for shared, non-serializable objects. - Returned objects are copied. Cached values are serialized and deserialized on each access, so a caller mutating a returned DataFrame won't corrupt the cached copy for other sessions. That safety costs some overhead on very large objects.
A worked example: caching a slow load
Here's a minimal pattern you can adapt. Run this with streamlit run app.py in your project directory (no special permissions needed beyond your normal Python environment):
import time
import pandas as pd
import streamlit as st
@st.cache_data(ttl=3600, show_spinner="Loading data...")
def load_data(source_path: str) -> pd.DataFrame:
# Stand-in for a slow CSV read, API call, or query.
time.sleep(5)
return pd.DataFrame({"x": range(1000), "y": range(1000)})
path = st.sidebar.text_input("Data source", value="data.csv")
df = load_data(path)
st.write(f"Loaded {len(df)} rows")
st.line_chart(df.set_index("x"))What to check when you run it:
- On the first run, the spinner shows for about five seconds while
load_dataexecutes. - Interact with any widget (or press R to rerun). The data appears instantly — the cache hit skipped the sleep.
- Change the text input value. The function re-runs, because the argument hash changed.
- Edit the body of
load_dataand save. The cache invalidates automatically.
The ttl=3600 parameter expires entries after one hour, which handles the freshness problem for data that changes upstream. The show_spinner argument gives users feedback during the first, uncached execution — small thing, big perceived-performance difference.
The trade-offs you actually need to manage
Memory. The cache lives in the app's RAM. Caching several large DataFrames across many parameter combinations can exhaust memory on a small deployment. Mitigations: set max_entries to bound the cache size, cache aggregated results instead of raw data, and watch actual usage (htop on Linux, Activity Monitor on macOS) while exercising the app with realistic inputs.
Side effects don't belong in cached functions. If your function writes to a database, sends an email, or appends to a log, caching means it only does that once per input set. Keep side-effecting code outside the cached boundary.
Staleness. Without a TTL, cached results persist until the inputs or code change, or until you clear the cache programmatically with load_data.clear() or via the app's cache menu. If your underlying data updates on a schedule, pick a TTL that matches it rather than leaving the default.
A quick decision rule
Use st.cache_data when a function is pure (same inputs, same output, no side effects), expensive relative to a rerun, and returns something serializable. Reach for st.cache_resource for connections and clients. Skip caching entirely for cheap functions — the hashing and copying overhead can exceed the work you're avoiding.
The actionable next step: profile your app by adding a time.sleep-free timing check or simply observing which interactions trigger long pauses, wrap the single most expensive pure function, and verify with the four checks above. Measure memory before and after if the data is large. One well-placed decorator usually delivers most of the win.
Note: behavior described reflects st.cache_data as established in Streamlit 1.x; verify parameter names against your installed version with streamlit version and the current docs before shipping.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.