In Streamlit, when do you use st.cache_data instead of st.cache_resource?
answer
- reruns make repeated work the default
- two decorators, two different return semantics
- one hands back a copy, the other the object
- connections and models must not be duplicated
- the store is server-side, not per session
basics
~20 sUse st.cache_data for serializable return values such as DataFrames and API responses, where each caller gets its own copy. Use st.cache_resource for a single shared object like a database connection or a loaded model, returned by reference.
solid answer
~50 sBoth decorators memoise a function so a rerun does not redo the work, but they differ in what they hand back. `st.cache_data` is for *data*: the return value is serialized into the cache and every call gets a fresh copy, so one session mutating the DataFrame it received cannot corrupt another session's view. `st.cache_resource` is for *handles*: a database connection, an HTTP client, a loaded ML model. Nothing is copied — every caller gets the identical object, which is the point, and which means it must be safe to use from several sessions at once. Both are keyed on the function's identity and its argument values, both accept `ttl` and `max_entries`, and both expose `.clear()`. Arguments prefixed with an underscore, such as `_conn`, are excluded from the key so unhashable handles can be passed in. Crucially the cache is server-side and shared across all sessions, not per user.
code
python · 9 linesimport streamlit as st
@st.cache_resource # one shared engine for the whole app
def get_engine():
return create_engine(os.environ["DB_URL"], pool_pre_ping=True)
@st.cache_data(ttl=600, max_entries=50) # a copy per caller, 10-min staleness
def load_orders(region: str):
return pd.read_sql(QUERY, get_engine(), params={"region": region})go deeper
Know that both decorators skip repeated work, and that data goes in st.cache_data while connections and models go in st.cache_resource. Recall that ttl bounds staleness.
Explain the copy-versus-reference semantics and the cache key made of function code plus argument values, including the underscore convention for unhashable parameters.
Demonstrate operational judgment: TTLs against warehouse cost, max_entries against unbounded keys, thread-safety of shared resources, and the leak risk of caching user-scoped data without identity in the key.
Own the freshness contract for the app — what staleness the business accepts, whether caching belongs in the app or in the warehouse's own result cache, and how multi-replica deployment changes the answer.
## Why caching is not optional here Streamlit re-executes the whole script on every interaction. Without caching, a query that takes four seconds takes four seconds again each time someone nudges a slider, once per viewer. Caching is the mechanism that makes the rerun model affordable, so "where are the cache boundaries" is a design question in every real Streamlit app, not a late optimisation. ## st.cache_data — cache the value ```python @st.cache_data(ttl=600) def load_orders(region: str) -> pd.DataFrame: return pd.read_sql(QUERY, engine, params={"region": region}) ``` On a miss, the function runs and its return value is stored. On a hit, Streamlit returns a **copy**. That copy semantics is the defining property: DataFrames, dicts and lists are mutable, and returning the same object to everyone would let one session's `df.drop(...)` poison the cached entry for every other session and every later rerun. The cost is serialization and copy time, which is negligible for a few megabytes and noticeable for very large frames. Use it for: query results, API responses, file parsing, feature engineering output, anything picklable that represents *data*. ## st.cache_resource — cache the object ```python @st.cache_resource def get_engine(): return sqlalchemy.create_engine(os.environ["DB_URL"], pool_pre_ping=True) ``` No copying, no serialization: every call returns the identical object. That is exactly what you want for a connection pool, an HTTP session, an S3 client, or a multi-gigabyte model you refuse to load twice. It is also what makes it dangerous — the object is genuinely shared across concurrent sessions running in separate threads, so it must be thread-safe, and any mutable state you park in it is global to the app. Use it for: connections and pools, clients, loaded models, tokenizers, expensive singletons. Do not use it as a shortcut to avoid copying a big DataFrame unless you accept that a mutation anywhere is a mutation everywhere. ## The cache key An entry is identified by the function (its module, name and body) plus the values of its arguments. Change the function's code and the old entries no longer match, which is why an edited app picks up new behaviour on reload. Arguments must be hashable for this to work; when they are not — a connection, a session object — prefix the parameter with an underscore and Streamlit excludes it from the key: ```python @st.cache_data def fetch(_conn, sku: str): return _conn.execute(...) ``` The caveat is the obvious one: if the excluded argument actually changes the result, you have just built a correctness bug. Add a real argument (a database name, a version tag) to disambiguate. ## Invalidation - **`ttl`** — expire entries after a duration; the standard way to bound staleness for warehouse queries. - **`max_entries`** — bound cache size, evicting least-recently-used entries; important when the key includes a user-supplied parameter with unbounded cardinality. - **`.clear()`** on the decorated function to drop that function's entries; `st.cache_data.clear()` to drop everything cached by that decorator. A "refresh data" button typically calls one of these and then reruns. - **`show_spinner`** controls the automatic "Running…" spinner on a miss, and can be set to a custom message. There is no dependency graph: Streamlit does not know that `load_orders` feeds `build_summary`. If you clear one and not the other you can serve a stale derived value, so put the `ttl` on the source function and let the derived one key off its output, or clear both. ## Scope: shared, and only within one process The cache lives in the server process and is shared by every session connected to it. Two implications matter operationally. First, caching a per-user filtered dataset under a key that omits the user is a data-leak shape — include the identity in the key or do not cache it. Second, if you run several replicas behind a load balancer, each has its own cache, so hit rates fall and `.clear()` only clears the replica that served the click. Restarting the app empties everything. ## Choosing, in one line each Can you pickle it and would a copy be fine? `st.cache_data`. Is it a live handle, a socket, a model, something you must not duplicate? `st.cache_resource`. If you are unsure and it is data, take `cache_data` — the copy protects you from an entire class of bug. ## Lineage note These two decorators arrived in Streamlit 1.18 and replaced the older `st.cache`, along with the interim `st.experimental_memo` (now `cache_data`) and `st.experimental_singleton` (now `cache_resource`). Interviewers sometimes still ask about `st.cache`; the honest answer is that it tried to serve both roles at once with hashing heuristics, produced confusing mutation warnings, and was split precisely because "copy the data" and "share the handle" are different jobs.
- What breaks if you cache a per-user filtered DataFrame with st.cache_data keyed only on a date range?Every viewer with the same date range gets whichever user's rows landed in the cache first, because the cache is server-side and shared across sessions. The user identity has to be part of the key, or the filtering must happen after the cache — better still, enforce it in the database with a per-user credential.
- You edited the function body but the app still returns the old numbers. Why?Check whether the stale value comes from a different cached function downstream, since Streamlit tracks no dependency graph between them, or whether the process is still running the old module. A code change to a cached function changes its key, so the function itself should miss — a derived cached function keyed on unchanged arguments will not.
- How does caching behave when you scale the app to three replicas?Each process keeps its own cache, so warm-up cost is paid three times, hit rates drop, and clearing the cache from the UI clears only the replica that handled that click. If freshness must be uniform, use short TTLs, an external cache for shared results, or accept per-replica variance and document it.
One is photocopying a report for each reader, the other is lending everyone the same key to the same door.
saying these in an interview costs you the question
- Uses st.cache_resource for DataFrames to avoid copy cost
- Thinks the cache is per user or per session
- Assumes clearing one cached function invalidates its callers
- Passes an underscore argument that genuinely changes the result
- Caches user-scoped rows under a key with no identity in it