Using Streamlit’s @st.cache_data and @st.cache_resource to Avoid Recomputing Expensive Operations
Learn how to decide when to cache a Streamlit function, what boundaries the cache respects, and how to verify that caching improves performance without introducing stale data.
14 Jul 2026, 13:11 UTC

Problem: repeated expensive computations in Streamlit reruns
Every time a user interacts with a Streamlit widget, the script reruns from top to bottom. If a function performs a costly operation—such as loading a large dataset, training a model, or querying a remote API—each rerun repeats that work, making the app feel sluggish.
Requirements
- Identify a pure function or resource‑initialization block whose output depends only on its input arguments and does not mutate external state.
- Ensure the function does not have hidden side effects (e.g., modifying a global variable, writing to a file, or altering mutable arguments).
- Determine whether the cached value is data (serializable) or a heavy resource (e.g., a database connection, ML model).
Smallest suitable design
Wrap the target function with the appropriate Streamlit caching decorator:
@st.cache_datafor functions that return data frames, lists, or other serializable objects.@st.cache_resourcefor objects that should be created once and reused, such as database connections, TensorFlow models, or Redis clients.
Streamlit automatically generates a cache key from the function’s arguments, stores the return value in‑process, and returns the cached result on subsequent calls with the same key.
Example configuration
import streamlit as st
import time
@st.cache_data(show_spinner=False)
def expensive_calculation(x: int) -> int:
"""Simulate a costly computation."""
time.sleep(2) # pretend this is heavy work
return x * x
st.title('Cache demo')
slider = st.slider('Select a number', 0, 10, 5)
result = st.write(f'Result: {expensive_calculation(slider)}')
Save the snippet as app.py and run it locally with:
streamlit run app.py
You need only read‑access to the directory containing app.py; no special permissions are required.
Trust and data boundaries
The cache lives in the memory of the Streamlit server process.
@st.cache_dataentries are shared across all user sessions unless you provide a custom hash function that incorporates session‑specific data.@st.cache_resourcecan be made session‑isolated by including a hashable identifier (e.g., a session ID) in the function arguments; otherwise the resource is shared.
Because the cache is in‑process, avoid storing sensitive information (PII, credentials) unless you are certain that the process is not accessible to other users or containers.
Operational checks
- Hit/miss visibility: Add a
printorloggingstatement inside the cached function. The log appears only on a cache miss, confirming that the function body is skipped on hits. - Size limits: Use the
max_entriesparameter (e.g.,@st.cache_data(max_entries=128)) to bound the number of cached items, orttlfor a time‑to‑live. - Manual invalidation: Call
st.cache_data.clear()orst.cache_resource.clear()from a button or admin page to force recomputation. - Monitoring: In production, wrap the decorator with a custom wrapper that increments Prometheus counters for hits and misses.
Failure modes
- Stale results: If the cached function reads a file or queries a database and the underlying data changes without the function’s arguments reflecting that change, the cached value becomes outdated.
- Side‑effect corruption: Mutating a mutable argument (e.g., appending to a list passed in) does not change the cache key, so subsequent calls receive the mutated result incorrectly.
- Memory pressure: Large return values increase both the cache key computation (hashing) and storage, potentially causing OOM kills in constrained environments.
- Multi‑worker discrepancy: When running behind a gunicorn or similar multi‑worker setup, each worker maintains its own in‑process cache, reducing effective hit rates.
When the design would change
- If the computation is truly idempotent and cheap (e.g., a simple arithmetic expression), the overhead of hashing and storing may outweigh any benefit; skip caching.
- For horizontal scaling with multiple workers, replace the in‑process cache with an external shared store such as Redis or Memcached, using
st.cache_dataas a thin wrapper that delegates to the external cache. - When security policies forbid keeping any user‑derived data in process memory (e.g., strict PCI‑DSS environments), avoid caching data that could contain sensitive fields; instead, recompute or use a secured external cache with proper access controls.
- If the function’s output is not hashable (e.g., a custom class without
__hash__), you must provide a customhash_funcsdictionary or refactor the function to return a hashable representation.
Practical way to verify the caching behavior
- Create a minimal app as shown above.
- Open the browser console or terminal where you launched
streamlit run. You should see theprintstatement (if added) only the first time a particular slider value is used. - Move the slider to a new value; observe the delay and the log reappearing.
- Return the slider to a previous value; the delay disappears and the log is absent, indicating a cache hit.
- To test size limits, set
max_entries=2and slide through three distinct values; the earliest value should be evicted and recomputed on the next selection.
These steps let you confirm that caching is working as expected without claiming any specific performance numbers from a test environment.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.