st.cache_data or st.cache_resource? Choosing the Right Streamlit Cache
Streamlit reruns your script on every interaction. Pick st.cache_data for serializable results and st.cache_resource for live connections and models—here is how to decide.
19 Feb 2026, 04:44 UTC

Every Streamlit developer meets the same performance wall. You build a dashboard, click a dropdown, and the entire script reruns from top to bottom. If that script loads a large Parquet file or opens a database connection on every interaction, the app feels stuck. Streamlit's answer is caching, but the library gives you two decorators—st.cache_data and st.cache_resource—and choosing the wrong one either raises serialization errors or leaks shared state across users.
The rule of thumb: use st.cache_data when your function returns serializable data you want memoized per input; use st.cache_resource when you need to persist a live, unserializable object across reruns and sessions.
The rerun tax and the serialization split
Streamlit reruns the whole script on every widget interaction. Cached functions skip re-execution when their inputs are unchanged, but the surrounding code still runs. Both decorators arrived in Streamlit 1.18 and replaced the older @st.cache, which is deprecated and removed in later releases.
The deciding factor is whether the return value can be serialized—converted to a byte stream and back, typically via Python's pickle module. st.cache_data stores a serialized copy of the result. st.cache_resource stores a reference to the actual object in memory.
st.cache_data: memoizing serializable results
Use st.cache_data for DataFrames, JSON, plots, or any object that can be pickled. Streamlit derives a cache key from the function arguments. If the arguments are unchanged, the stored value is returned without running the function body.
import streamlit as st
import pandas as pd
@st.cache_data
def load_sales(path: str) -> pd.DataFrame:
print("Reading from disk...") # runs only on cache miss
return pd.read_parquet(path)
When a user changes a widget, load_sales("sales.parquet") returns the cached DataFrame and the print statement does not repeat. Because the value is serialized, each caller receives a copy; mutating the returned DataFrame does not corrupt the cache.
Cache keys come from function arguments. Mutable arguments such as lists or dictionaries are unhashable—they cannot form a stable key—and will raise errors unless you convert them to immutable types (e.g., tuples) or supply explicit hash_funcs.
st.cache_resource: keeping live objects alive
Use st.cache_resource for objects that cannot be pickled: database connections, machine-learning models, or API clients. Streamlit persists the actual instance across reruns and shares it across sessions.
import streamlit as st
import sqlite3
@st.cache_resource
def get_connection(db_path: str) -> sqlite3.Connection:
return sqlite3.connect(db_path)
Calling get_connection("app.db") twice returns the same sqlite3.Connection object. Attempting to cache this with st.cache_data would fail because the connection is not serializable.
Worked example: SQLite queries without reconnecting
A common pattern combines both decorators: a resource for the connection, and data caching for query results. Because a connection object is unhashable, pass it to the data-cached function with a leading underscore so Streamlit excludes it from the cache key.
import streamlit as st
import pandas as pd
import sqlite3
@st.cache_resource
def get_connection() -> sqlite3.Connection:
return sqlite3.connect("app.db")
@st.cache_data
def run_query(_conn, sql: str) -> pd.DataFrame:
return pd.read_sql(sql, _conn)
conn = get_connection()
df = run_query(conn, "SELECT * FROM orders LIMIT 100")
st.dataframe(df)
Here, run_query re-executes only when sql changes, not when the script reruns. The connection is created once and reused.
Trade-offs, limitations, and how to verify
Caching is not free. Three limitations matter in production:
- Side effects: Do not write to disk, send emails, or call mutable external APIs inside a cached function body. On a cache hit, the body does not run, so the side effect is skipped.
- Invalidation: Cached values persist until the app source changes, you call
.clear(), or a TTL (time-to-live) /max_entriespolicy evicts them. If upstream data changes but your code does not, the cache will serve stale results. - Version drift: Default limits and eviction behavior can differ between Streamlit releases. Verify against your installed version before relying on specific defaults.
To verify the split in your own app, run it from your terminal with streamlit run app.py (replace app.py with your script name). No special permissions are required beyond normal file access. After the app starts, interact with a widget and watch the terminal output: a timestamp printed inside a cached function should appear only on the first run, not on subsequent reruns with unchanged inputs. The risk here is confirmation bias—if you change an input argument, the cache correctly misses and the function reruns, so test with identical inputs.
For the resource, check identity with conn1 is conn2 inside the app or logs. To force recomputation, add a button whose callback calls st.cache_data.clear() or st.cache_resource.clear(); the next interaction should trigger re-execution.
Actionable takeaway: Before adding a decorator, ask one question—can this return value be pickled? If yes, reach for st.cache_data and keep heavy I/O inside the function. If no, reach for st.cache_resource and treat the object as shared, long-lived state. Then verify with a timestamp or identity check, and pin your Streamlit version so cache behavior stays predictable.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.