Streamlit Caching - Interview Questions

3 interview angles 4 min read source

Streamlit Caching - Interview Questions

1. Why does Streamlit need caching?

Streamlit re-runs the entire script on every interaction. Without caching, every button click would re-load the CSV, re-query the database, re-fit the model. Caching is how you make a Streamlit app fast.

Two decorators:

  • @st.cache_data — for data (DataFrames, lists, dicts, JSON, numbers, strings — anything serializable).
  • @st.cache_resource — for resources (database connections, ML models, HTTP clients — singletons that should not be copied).

Older @st.cache is deprecated; don’t use it in new code.


2. What is @st.cache_data?

Caches the return value of a function. Streamlit hashes the arguments, looks up the result, and returns a copy so callers can’t mutate the cache.

@st.cache_data
def load_data(path: str) -> pd.DataFrame:
    return pd.read_csv(path)

df = load_data("sales.csv")  # first call: reads CSV
df = load_data("sales.csv")  # second call: returns cached copy

Use it for: file reads, API calls returning JSON, expensive transformations, query results.

Don’t use it for: anything you can’t pickle (open file handles, DB connections, models with non-serializable internals).


3. What is @st.cache_resource?

Caches a single shared instance — no copying, no hashing of return value. All callers get the same object.

@st.cache_resource
def get_db_connection():
    return psycopg.connect(DATABASE_URL)

@st.cache_resource
def load_model():
    return SentenceTransformer("all-MiniLM-L6-v2")

conn = get_db_connection()  # same conn across all reruns and all users
model = load_model()        # loaded once per process

Use it for: database connections, ML models in memory, HTTP session objects, anything where “create once, share forever” is the right semantics.

Critical: the cached object is shared across all users hitting this Streamlit process. Don’t put per-user state in here.


4. When do you use cache_data vs cache_resource?

Question Answer Decorator
Returns a DataFrame, list, dict, scalar? Yes cache_data
Returns a DB connection / model / client? Yes cache_resource
Should each caller see an independent copy? Yes cache_data
Must all callers share the same instance? Yes cache_resource
Object can be pickled? Yes cache_data
Object can NOT be pickled? Then cache_resource

Mnemonic: data = “I want a copy.” resource = “I want the original.”

If you wrap a DB connection in @st.cache_data, you’ll either crash on pickling or return a copy that points to a closed socket. If you wrap a DataFrame in @st.cache_resource, mutating it in one callback corrupts every other user’s view.


5. How does Streamlit hash the arguments?

Streamlit hashes function arguments to look up the cache. Built-in types (str, int, float, list, dict, DataFrame, …) hash automatically.

For unhashable types or custom objects, prefix the argument name with _:

@st.cache_data
def fetch(_session: requests.Session, url: str) -> dict:
    return _session.get(url).json()

Underscore-prefixed args are excluded from the hash. Use this for objects you can’t or don’t want to hash (clients, connections passed in).

You can also pass hash_funcs={SomeType: lambda x: x.id} to customize hashing per type.


6. How do you set TTL or invalidate the cache?

@st.cache_data(ttl=3600)              # 1 hour
def load_prices():
    return fetch_from_api()

@st.cache_data(ttl="10m", max_entries=100)
def query(symbol: str):
    return db.query(symbol)
  • ttl — seconds, timedelta, or string ("10m", "1h").
  • max_entries — LRU eviction past this count.

Manual invalidation:

load_prices.clear()        # clear this function's cache
st.cache_data.clear()      # clear all cache_data caches
st.cache_resource.clear()  # clear all cache_resource caches

Add a “Refresh” button:

if st.button("Refresh"):
    load_prices.clear()
    st.rerun()

7. What is show_spinner?

@st.cache_data(show_spinner=True) shows a “Running …” spinner during cold cache misses. Defaults to True. Pass a string for a custom message, or False to suppress:

@st.cache_data(show_spinner="Loading sales data...")
def load_sales():
    ...

@st.cache_data(show_spinner=False)
def quick_lookup(k):
    ...

Disable for fast lookups where the spinner just flashes annoyingly.


8. Common caching mistakes

# Mistake 1: caching a function that mutates external state
@st.cache_data
def log_query(q):
    db.execute("INSERT INTO log VALUES (?)", q)  # only runs on cold cache!

# Mistake 2: cache_data on a connection
@st.cache_data            # WRONG — connection is not picklable / will be copied
def get_conn():
    return create_engine(DSN)

# Mistake 3: cache_resource on per-user data
@st.cache_resource         # WRONG — shared across all users
def user_dashboard(user_id):
    return load_user_data(user_id)

# Mistake 4: caching the result, then mutating it
@st.cache_data
def load():
    return pd.read_csv("a.csv")

df = load()
df.drop(columns=["x"], inplace=True)   # cache_data returns a copy, so this is fine
                                       # but with cache_resource it would corrupt other callers

Rule: cache pure functions. Side effects don’t belong inside a cached function.


9. How does caching interact with st.session_state?

They serve different scopes:

  • Cache is per-process, shared across all sessions and users hitting this Streamlit instance.
  • st.session_state is per-session (per browser tab).

Use cache for “expensive to compute, same answer for everyone” (loaded model, lookup table). Use session state for “this user’s current selection.”

Caching the result of a user-specific query? Either include the user id in the cache key (which works but pollutes the cache across users) or just put it in session state.


Interview angle

  • st.cache_data or st.cache_resource?” - cache_data for serialisable return values, returning a copy per call so mutation can’t corrupt the cache. cache_resource for unserialisable shared objects - database connections, ML models - returned by reference and shared across sessions.
  • “What happens if you use the wrong one?” - caching a connection with cache_data tries to serialise it and fails or misbehaves; caching a DataFrame with cache_resource shares one mutable object across all users.
  • “How is the cache key computed?” - from the function’s arguments and code. Unhashable arguments need an underscore prefix to be excluded, and ttl bounds staleness.