Choosing Between st.cache_data and st.cache_resource in Streamlit
Pick the right Streamlit cache decorator: st.cache_data for copy-safe data, st.cache_resource for shared connections and models.
23 Oct 2025, 20:36 UTC

The Decision: Store a Copy or Share a Reference?
Every Streamlit interaction reruns the whole script from top to bottom. Without caching, a large CSV load or a model initialization runs again on every button click and slider drag, and latency collapses. Streamlit ships two supported caching decorators, and picking the wrong one produces either pickling errors or cross-session state bugs.
The rule of thumb: use st.cache_data for functions that return serializable data — DataFrames, lists, dicts, scalars — and st.cache_resource for objects that should exist once per process, such as database connections, ML models, and thread pools. Both have been the stable caching API since Streamlit 1.18.
Side-by-Side Comparison
| Aspect | st.cache_data | st.cache_resource |
|---|---|---|
| Intended for | Serializable results: DataFrames, lists, dicts, scalars | Objects that should exist once: connections, models, thread pools |
| Storage | Serialized (pickle-like) copy | Single shared reference |
| What a rerun receives | A fresh copy of the result | The same object instance |
| Session isolation | Safe by default; mutations stay local to one session | Shared; mutations affect every session |
| Overhead | Serialization and copying, grows with object size | None beyond first creation |
| Typical failure if misused | Pickling errors on connections or file handles | Accidental shared mutable state |
Both decorators accept the same control knobs: ttl (seconds before eviction), max_entries (bounded cache size), show_spinner, and a per-function .clear() for manual resets.
Trade-offs in Practice
Copy-on-read is safe but costs memory
st.cache_data pickles the return value and hands each rerun a copy. Two users filtering the same DataFrame cannot corrupt each other's results. The price is memory: ten concurrent sessions each holding a copy of a large DataFrame can spike the server process's memory usage. For very large objects, cache a smaller derived result instead of the raw payload.
Shared references are fast but need guarding
st.cache_resource stores the object once and returns the same reference to every rerun and session. There is no serialization cost, which is exactly why connections and models belong here. The catch: concurrent sessions may call the object simultaneously, so it must be thread-safe. If sessions mutate the resource, add a lock or return copies from the cached function.
Unhashable arguments and the underscore prefix
Cache keys are derived from the function's code and its arguments. Passing an unhashable object — an open connection, for example — into a cached function raises an error. Prefix the parameter name with an underscore, as in _conn, to exclude it from the key while still receiving the value inside the function.
A Concrete Pattern: One Connection, Cached Queries
This pattern combines both decorators: the connection is cached as a resource, and query results are cached as data with a ten-minute TTL so stale rows eventually refresh.
import streamlit as st
import pandas as pd
import time
# Shared once per process; no reconnect on reruns.
@st.cache_resource
def get_connection():
time.sleep(2) # simulate expensive setup
return "DB_CONNECTION_OBJECT"
# _conn is excluded from the cache key; the query drives invalidation.
@st.cache_data(ttl=600)
def run_query(_conn, sql):
time.sleep(1) # simulate data retrieval
return pd.DataFrame({"result": [f"rows for {sql}"]})
conn = get_connection()
sql = st.text_input("Query", "SELECT * FROM sales")
if st.button("Run"):
st.write(run_query(conn, sql))
Expected behavior: the first click takes roughly three seconds (connection setup plus query). Later clicks with the same query text are instant. Editing the text triggers only the one-second query path, because the connection is reused and the new query string forms a new cache key.
How to Validate the Behavior
- Print a timestamp inside each cached function. Rerun via a widget: the timestamp should stay frozen for identical inputs and change when inputs change.
- Set
ttl=5onrun_query, wait six seconds, rerun, and confirm the function body executes again. - With
st.cache_resource, mutate the returned object in one rerun and observe the change persisting into the next — proof of shared-reference semantics. - Call
run_query.clear()during development to force a cold run on demand.
Limitations and Version Notes
st.cache_datacannot serialize connections, sockets, or file handles; caching those raises a pickling error. Usest.cache_resourceinstead.- Mutating a
cache_resourceobject affects all sessions; guard with locks or return copies. - Neither decorator detects upstream database changes.
ttlis the practical refresh mechanism, not a change-detection system. - Large cached objects increase memory pressure on the Streamlit server process; bound growth with
max_entriesorttl. - The legacy
st.cacheAPI and thest.experimental_memo/st.experimental_singletonnames are deprecated. Confirm decorator signatures against the documentation for your installed Streamlit version before relying on parameter details.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.