Choosing the Right Cache for Streamlit: st.cache_data vs st.cache_resource vs st.session_state
When building a Streamlit app you’ll often need to reuse expensive work. This guide walks you through the four caching options – st.cache_data, st.cache_resource, st.session_state, and no caching – and shows how to pick the right one for multi‑user deployments, memory limits, and per‑user isolation.
17 May 2026, 01:56 UTC

What you need to decide
When an expensive function runs in a Streamlit app you can choose one of four options:
st.cache_data– memoize the return value and give each caller a fresh copy.st.cache_resource– memoize the object itself and share the same instance across all sessions.st.session_state– keep data only for the current user session.- No caching – recompute on every rerun.
The decision depends on four constraints:
- Multi‑user deployment – Do you need the same data for all users or isolated copies?
- Server memory limits – How much memory can you afford to reserve for cached objects?
- Staleness tolerance – How often can the data become out‑of‑date before you must refresh?
- Per‑user isolation vs. shared results – Do you want each user to see the same data or maintain separate state?
Quick‑look comparison
| Option | Shared? | Copy on read? | Suitable for | Typical use |
|---|---|---|---|---|
| st.cache_data | No – each user gets a copy | Yes – pickled copy | Large immutable data (DataFrames, arrays, API responses) | Data loading, preprocessing |
| st.cache_resource | Yes – same instance for everyone | No – original object returned | Mutable resources (DB engines, ML models) | Database connection, model inference |
| st.session_state | No – per‑user only | No – same reference in session | UI state, per‑user selections | Form values, toggle flags |
| No caching | No | No | Always fresh data, low cost operations | Quick lookups, dynamic data |
Trade‑offs in detail
st.cache_data
• Serialization cost – the return value is pickled. Large objects incur CPU and memory overhead on each call.
• Isolation – each user gets a separate copy, so accidental mutation in one session does not affect others.
• Memory usage – one copy per user. Use max_entries or ttl to limit growth.
st.cache_resource
• No copying – the same object instance is returned, avoiding serialization overhead.
• Shared mutable state – if the object is mutated, all users see the change. Guard with locks or immutable patterns.
• Memory footprint – only one instance, but it lives for the life of the process.
st.session_state
• Not a cache – data lives only for the duration of the user session. It is ideal for UI state but not for expensive data that can be reused.
• Isolation – perfect per‑user separation, but you pay the cost of recomputation for each new user.
No caching
• Always fresh – no stale data but every rerun triggers the expensive work.
• Best for low‑cost or truly dynamic data where recomputation is cheap.
Concrete implementation pattern
Below is a minimal Streamlit app that demonstrates a common pattern: load a CSV once per user with st.cache_data and create a database engine once for all users with st.cache_resource.
The example also shows how to invalidate caches programmatically using .clear() and how to verify that the caching behaviour matches expectations.
# app.py
import streamlit as st
import pandas as pd
import time
from sqlalchemy import create_engine
# ---------- 1️⃣ Data loader ---------------------------------
@st.cache_data(ttl=3600, max_entries=10)
def load_csv(path: str) -> pd.DataFrame:
"""Read a CSV file and return a DataFrame.
The function is memoized; the result is pickled and a fresh copy is
given to each caller. The cache expires after 1 hour (ttl) or when
10 distinct keys are stored (max_entries)."""
st.info(f"Loading CSV from {path} – this runs only on cache miss.")
df = pd.read_csv(path)
return df
# ---------- 2️⃣ Database engine ---------------------------------
@st.cache_resource
def get_engine() -> create_engine:
"""Return a SQLAlchemy engine.
The same engine instance is shared across all users.
This is suitable for objects that cannot be pickled."""
st.info("Creating new database engine – runs once per process.")
return create_engine("sqlite:///example.db")
# ---------- 3️⃣ UI --------------------------------------------
st.title("Streamlit Caching Decision Demo")
# Load data (cached per user)
if st.button("Load data"):
df = load_csv("data/sample.csv")
st.write("Data loaded:", df.head())
# Show engine ID (shared across users)
if st.button("Show engine ID"):
engine = get_engine()
st.write("Engine id:", id(engine))
# Invalidate caches
if st.button("Clear caches"):
load_csv.clear()
get_engine.clear()
st.success("Caches cleared – next click will recompute.")
# Verify st.session_state isolation
if st.button("Add to session state"):
st.session_state.setdefault("counter", 0)
st.session_state.counter += 1
st.write("Session counter:", st.session_state.counter)
**Where to run** – Save the code as app.py and run streamlit run app.py from a terminal. You need a data/sample.csv file in the same directory. The database engine uses an SQLite file; no special permissions are required.
**Expected checks** –
- Press Load data once: you should see the “Loading CSV” message and the DataFrame. Press it again: the message should not appear, indicating the cache hit.
- Press Show engine ID in two separate browser tabs (simulating two users). The
id()should be identical, proving a shared instance. - Press Add to session state in each tab. The counter should increase only within that tab, demonstrating per‑user isolation.
- Press Clear caches and then Load data again: the “Loading CSV” message should reappear, confirming programmatic invalidation.
**Limitations** –
- Both
st.cache_dataandst.cache_resourcelive only in the process that runs the app. If you deploy with multiple workers (e.g., Gunicorn or Streamlit Cloud’s multi‑worker mode), each worker has its own cache, so hit rates vary. - Large cached objects can quickly exhaust server memory. Use
max_entriesor store only aggregates. - Mutable objects returned by
st.cache_resourcecan lead to race conditions if accessed from multiple threads. Use thread‑safe patterns or external locks if needed. - Objects that cannot be pickled (open sockets, certain model handles) must go into
st.cache_resourceor be recreated on each run.
**Verification checklist** – before deploying, run the following sanity tests locally:
- Execute
streamlit run app.pyand confirm the UI behaves as described above. - Check the Streamlit logs for the “Loading CSV” and “Creating new database engine” messages to ensure caching occurs.
- Restart the app and verify that all caches are cleared (messages appear again).
- Deploy to a single‑worker environment and confirm that two separate users (different browser windows) share the same engine instance.
By mapping your constraints to the table above and following the concrete pattern, you can avoid accidental data duplication, prevent stale data, and keep your Streamlit app responsive and resource‑efficient.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.