Streamlit cache misdiagnosis: when st.cache_data and st.cache_resource misbehave
A diagnostic guide for Streamlit caching problems: wrong decorator choice, unhashable arguments, and per‑process sharing causing data leaks or reload loops.
14 Jan 2026, 08:22 UTC

The problem you are seeing
Your Streamlit app reloads a large model on every widget interaction, or returns the same dataframe to every user, or throws pickling errors after you added caching. Streamlit reruns the whole script on each interaction. Cached functions are the only way to avoid recomputation, but the wrong decorator or argument handling makes caching harmful.
The useful takeaway: use st.cache_data for serializable results that can be recomputed per input, and st.cache_resource for singleton objects that must be created once per app process. Cache is per process, not per user.
Recognizable conditions
- Expensive load runs on every rerun despite a decorator. Print statements inside the function fire each time.
- Pickling or serialization errors when returning a client, model handle, or connection pool.
- Users see each other's data or stale results that do not change when inputs change.
- Cache key errors for dict, dataframe, or mutable default arguments.
Cause and symptom table
| Symptom | Likely cause | Decorator involved |
|---|---|---|
| Function runs on every interaction | No cache hit: arguments differ each run or function not decorated correctly | st.cache_data or st.cache_resource |
| Pickling error / TypeError on return | Non-serializable object returned from st.cache_data | st.cache_data |
| Model or DB client recreated per user | Singleton created with st.cache_data, which copies on each hit | st.cache_data |
| Stale data shared across users | User‑specific input omitted from cached function arguments, or data cached with st.cache_resource | Both |
| Unhashable type error | Dict, list, dataframe passed as argument without hashing support | Both |
Ordered diagnostic checks
1. Confirm which decorator is used and what is returned
Open the app script where the expensive work lives. Identify the decorator and the return type.
# data loading
@st.cache_data(ttl=600, max_entries=3)
def load_data(source_id):
...
# singleton resource
@st.cache_resource
def get_model():
...Run the app locally with streamlit run app.py. Add a print at the top of each decorated function. The console should show execution only on cache miss.
2. Check argument hashability and stability
List all parameters. Mutable defaults, dicts, or dataframes passed by reference can produce unstable keys. Convert to hashable forms, e.g., tuple of sorted items or a string identifier.
3. Verify cache scope
Cache lives per app process. Open two browser sessions with different inputs. If a cached result does not vary with user input, a user‑specific value is missing from the function signature.
4. Inspect TTL and max entries behavior
For st.cache_data, ttl and max_entries control eviction. If data should expire, ttl must be set. If memory grows, max_entries limits stored results.
Fixes tied to findings
Reload loop on every interaction
Ensure the function is decorated and arguments are stable. Remove non‑deterministic values like time.now() from arguments. If you need fresh data, pass an explicit refresh token from a widget.
@st.cache_data
def get_data(source, refresh_token):
...Risk: adding a token forces recompute; use sparingly.
Pickling errors for models or clients
Move the object to st.cache_resource. Do not return it from st.cache_data.
@st.cache_resource
def get_db_client():
return create_client()Risk: resource is shared across all users in the process. Ensure the client is thread‑safe.
Stale or leaked data between users
Include all user‑specific inputs in the cached function arguments. Do not cache the final rendered dataframe with st.cache_resource. For data, prefer st.cache_data.
Rollback: remove the decorator to restore per‑run behavior if data leakage is suspected.
Unhashable arguments
Transform inputs before caching. Example: convert dict to frozenset of items or use a string key.
def normalize(kwargs):
return tuple(sorted(kwargs.items()))Risk: normalization can hide semantic differences.
Limitations and practical verification
Argument hashing and TTL semantics are version sensitive. Behavior of st.cache, st.cache_data and st.cache_resource changed across major Streamlit releases. Do not assume identical behavior across versions.
Cache is per process, not per user. In multi‑worker deployments, each worker has its own cache.
Changes to code do not automatically invalidate the cache. The cache key is based on arguments and code identity in current versions; manual clear may be required after code changes.
Verification steps you can run locally:
- Create a minimal app with a
st.cache_datafunction returning current time. Trigger rerun with a button. The value should stay constant until arguments change. - Create a minimal app with a
st.cache_resourcefactory returning an object with a unique id. Confirm the id remains identical across reruns and widget interactions. - Add
printstatements inside cached functions and compare execution frequency with Streamlit console output.
Escalation criteria
Escalate when:
- Resource objects are not thread‑safe and shared via
st.cache_resourcecauses race conditions. - You need per‑user isolation for large data. Consider moving data loading out of cache or using session state with explicit keys.
- Cache memory grows unbounded despite
max_entriesandttl. Profile process memory and review eviction policy. - Unhashable arguments cannot be normalized without losing correctness. Refactor to use explicit identifiers.
If the app runs behind multiple replicas, coordinate cache invalidation externally. Streamlit cache does not synchronize across processes.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.