# Classify and fix Streamlit cache failures
Use this when a Streamlit app crashes with a hashing error from `@st.cache_data` /
`@st.cache_resource`, or when cached results misbehave: stale data that never
refreshes, or mutations in one session leaking into other users' views. These are
three distinct failure families with different fixes — classify from the error or
the symptom first.
## 1. Read the failure
Hashing crashes come from `streamlit.runtime.caching.cache_errors` and name the
offending argument and its type:
```
streamlit.runtime.caching.cache_errors.UnhashableParamError: Cannot hash argument 'documents' (of type builtins.list) in 'setup_retriever'.
```
`UnhashableTypeError` (no argument named) is the same family: Streamlit could not
build a cache key for the call. Behavioral failures have no traceback: the
function silently doesn't re-run when you expect it to, or mutating a cached
object corrupts what other sessions see.
## 2. Classify
- **A. UnhashableParamError / UnhashableTypeError naming an argument.** Streamlit
cannot hash that argument into the cache key. Typical culprits: DB connections
and clients, ML models, LangChain `Document` lists, DataFrame subclasses with
custom state, plain Python objects whose default `__reduce__` references
unhashable internals, functions or lambdas passed as arguments.
- **B. Cache never refreshes (stale results).** The cache key is built from the
call's arguments, the function's source code, and external variables the
function reads. Editing the function body invalidates its cache automatically;
changes in the outside world (a new CSV on disk, new rows in a database) do
NOT. Expecting external changes to appear without invalidation is the classic
mistake.
- **C. Mutations leak across sessions.** `@st.cache_resource` returns the SAME
global object to every session; `@st.cache_data` returns a fresh pickle copy
per call. If two users see each other's edits, or appending to a cached list
corrupts later runs, you are mutating a shared `cache_resource` result.
## 3. Fix A: make the argument hashable or exclude it
**Option 1 — exclude it from the key.** Rename the parameter with a leading
underscore; Streamlit skips hashing it:
```python
import streamlit as st
@st.cache_data(ttl=3600)
def load_subset(_connection, table, limit):
# _connection is excluded from the cache key.
return _connection.query(table).head(limit)
```
Call sites are unaffected — positional and keyword calls keep working; only the
parameter name in the definition changes. Caution: the function will NOT re-run
when only the excluded argument changes, so pair this with a `ttl` if the
underlying resource can change.
**Option 2 — teach Streamlit how to hash the type** with a custom hash function:
```python
@st.cache_resource(hash_funcs={Data: lambda d: d.payload})
def transform(data):
...
```
**Option 3 — restructure.** Move the unhashable object out of the signature:
create it inside the function, or read it from `st.session_state`, so the cache
key only covers hashable inputs.
Rule of thumb: if the argument identifies *which* data to load (a table name, a
date range), keep it in the key with a real hash. If it is a handle to *how* to
load (a connection, a client), exclude it with `_`.
## 4. Fix B: schedule invalidation instead of hoping for it
- Data that changes on its own schedule gets a `ttl`: `@st.cache_data(ttl=3600)`
re-runs the function at most once per hour. Between expiries, widget-driven
reruns serve the cached value.
- Manual "Refresh" button: clear that function's cache, then rerun:
```python
@st.cache_data(ttl=600)
def load_data():
return pd.read_csv(DATA_URL)
if st.button("Refresh data"):
load_data.clear() # clears only this function's cache
st.rerun()
```
`st.cache_data.clear()` and `st.cache_resource.clear()` wipe every cached
function of that type — coarser; prefer the per-function `.clear()`.
## 5. Fix C: stop mutating shared objects
- Treat `cache_resource` return values as read-only singletons. If code must
transform the result, copy first (`df.copy()`, `copy.deepcopy`), or move the
mutable working copy into `st.session_state` per user.
- Never cache per-user values with the global caches. A `load_for_user(user_id)`
under `@st.cache_data` shares memory across all sessions — per-user state
belongs in `st.session_state`.
- Sanity check: call the function twice. If `result_a is result_b` (identical
object), it's a shared resource (`cache_resource`). If they're equal but not
identical, it's a fresh copy (`cache_data`).
## 6. Checklist
1. Hashing crash → name the argument from the error; `_`-prefix it, add a
`hash_funcs` entry, or remove it from the signature.
2. Stale data → the key doesn't watch the outside world; add `ttl` or a
per-function `.clear()` refresh path.
3. Cross-session corruption → you're mutating a `cache_resource` singleton; copy
before mutating, or move the mutable state to `st.session_state`.
4. Editing a cached function's code invalidates its cache automatically — no
manual clearing needed after changing the function itself.