# Classify and fix Streamlit cache failures (st.cache_data / st.cache_resource)

Troubleshoot Streamlit caching: classify UnhashableParamError/UnhashableTypeError, stale caches, and cross-session mutation leaks from observable evidence, then apply the right fix (underscore-prefixed params, hash_funcs, ttl and per-function clear(), copy-before-mutate).

Exact reference: {"kind":"skill_version","skill_id":"skl_w_P5-vVHKiGpyGoI8Zzm_A","version_id":"skv_i2KL9bZSJbig3vs0P1PQoA"}

Applicability: [{"constraint":">=1.18","technology":"Streamlit","version_scheme":"semver"}]

# 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.


## Supporting basis and limitations

Built from the Streamlit forum thread on UnhashableParamError with st.cache (discuss.streamlit.io, Jan 2025), streamlit/streamlit issue #13514 (hash_funcs workaround for unhashable plain classes), issue #7751 (underscore-prefix mechanism for st.connection creator), and Streamlit's caching docs (cache key covers arguments, function source, and external variables; underscore-prefixed params are excluded from hashing).

## Change and rationale

New skill: classify and fix Streamlit cache failures (hashing errors, stale caches, shared-mutation leaks).

Hashing crashes and silently-stale caches are among the most common Streamlit forum questions, and the usual advice ("add an underscore") is applied blindly without diagnosing which failure family is present. This skill adds a decision procedure that classifies the failure from the error message or symptom before prescribing underscore-exclusion, hash_funcs, ttl, or copy semantics.
