Diagnosing @st.cache_data misses, stale data and memory growth in Streamlit apps
A practical diagnostic guide for Streamlit @st.cache_data problems: unexpected recomputation, stale data, unhashable errors and memory growth, with ordered checks, fixes and escalation criteria.
07 Sept 2026, 23:30 UTC

Your Streamlit app recomputes an expensive function on every interaction, the UI resets unexpectedly, or uploaded data never appears to change. That is usually @st.cache_data keying, invalidation or memory behavior, not a general rerun bug.
Recognizable condition
Typical signs are:
- Expensive load or transform runs again when a widget changes unrelated to the data.
- Data displayed does not update after a file upload or parameter change.
- Memory climbs steadily and the app slows or OOMs.
- Unhashable type error on startup or first call.
Cause / symptom table
| Symptom | Common cause |
|---|---|
| Re-runs on every widget change | Missing @st.cache_data or function not decorated |
| Data never updates | Cache key lacks source dependency, e.g., file not passed as argument |
| High memory usage | Large objects cached without max_entries or ttl limits |
| Unhashable type error | Caching with unhashable arguments such as dict, list, DataFrame view |
| Stale UI after upload | Function ignores uploaded file input, key does not change |
Ordered checks
Verify decorator presence. Confirm the function uses @st.cache_data, not a plain def. For shared client resources such as DB connections, @st.cache_resource is the intended decorator.
Check argument hashability. Inspect all parameters. Lists, dicts and some pandas objects are unhashable by default. Convert to tuples or pass hashable proxies.
Confirm invalidation sources are arguments. If the function reads a file, a URL, or a widget value, that value must be an explicit argument. Reading from a global or st.session_state inside the cached function will not change the key.
Review ttl and max_entries. Check decorator arguments: ttl seconds for freshness, max_entries for eviction. Large returns with no limits can grow memory.
Monitor cache events. Run the app from a terminal in the project directory with debug logging to see hit/miss decisions. Command:
streamlit run <script.py> --logger.level=debug. Look for cache hit and cache miss messages for the function name. No special permissions required. Risk: debug logs are verbose.
Fixes tied to findings
Missing decorator. Add @st.cache_data above the function. Example shape:
@st.cache_data(ttl=600, max_entries=8)
def load_data(path: str):
return pd.read_parquet(path)
Risk: first call still computes; subsequent calls reuse result.
Incorrect key. Add missing arguments or supply hash_funcs. Example for a dict argument:
@st.cache_data(hash_funcs={dict: lambda d: tuple(sorted(d.items()))})
def transform(params: dict):
...
Stale data. Include the data source in the signature. For an uploaded file, pass file.name and file.getvalue() or a hash of the bytes. Set a sensible ttl aligned with data freshness requirements.
Memory pressure. Set max_entries to bound the cache, lower ttl, or move heavy immutable clients to @st.cache_resource. Avoid caching huge intermediate objects; cache the final result only.
Unhashable error. Convert mutable inputs to immutable forms before the call, e.g., tuple(my_list), or use hash_funcs. Do not cache widget objects directly; widget return values are not stable keys.
Verification
- Add a print statement inside the cached function. It should execute once per unique input set, then be silent on repeats.
- Call st.cache_data.clear() in an admin button or console to force invalidation, then confirm the next call re-executes once.
- With debug logging enabled, confirm cache hit on repeat calls and cache miss after argument change or ttl expiry.
Limitation: mutable objects returned from a cached function are shared across callers. In-place modifications affect all users of that cache entry. Return copies if mutation is expected.
Escalation criteria
Escalate if recomputations persist after the checks above, cache miss rate stays above ~80% for stable inputs, or memory usage exceeds available RAM.
- File a minimal reproducible example on the Streamlit GitHub repo with decorator, inputs and observed logs.
- Consider switching to @st.cache_resource for shared clients and moving heavy computation to an external service or job queue.
Long TTL values can serve stale data. Align TTL with business freshness needs and document invalidation behavior for operators.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.