The idea
The rerun model has a cost: every click runs the whole script, including the slow parts. If loading your data takes 3 seconds, every slider move takes 3 seconds. Caching fixes this: Streamlit remembers a function's result, and the next time it's called with the same arguments, returns the stored result instantly instead of running the function again.
A cache is a sticky note on the fridge with yesterday's answer. Before you redo a long calculation, you check whether the answer to exactly this question is already written down.
st.cache_data: for data
Loading a big CSV file can take seconds, and because the whole script reruns every time a widget changes, the file gets loaded again on every single click. The fix is to put the loading in a function and mark it with @st.cache_data. Streamlit then remembers the result and hands it back instantly next time:
import streamlit as st
import pandas as pd
@st.cache_data
def load_sales(path):
st.write("Loading data from disk...") # visible only when the function really runs
return pd.read_csv(path)
sales = load_sales("sales.csv")
st.write(f"{len(sales)} order lines loaded")
st.slider("Pick a number", 0, 10, 5)
On the first run you see "Loading data from disk...". Move the slider: the message disappears, because load_sales("sales.csv") was answered from the cache. Your function's body only runs when the cache doesn't have an answer.
st.cache_data is for functions that return data: DataFrames, lists, dicts, numbers, results of API calls and database queries. Each call gets a copy of the cached value, so code that modifies the returned DataFrame can't corrupt the cache.
How the cache key works
What if the function's answer depends on its arguments? Revenue by category and revenue by channel are different results. The cache handles this by storing one result per combination of arguments:
import streamlit as st
import pandas as pd
@st.cache_data
def revenue_by(column):
sales = pd.read_csv("sales.csv")
completed = sales[sales["status"] == "completed"]
return completed.groupby(column, as_index=False)["revenue"].sum()
choice = st.radio("Group by", ["category", "channel", "city"], horizontal=True)
st.dataframe(revenue_by(choice), hide_index=True)
The first time each option is chosen, the function runs; after that, switching back and forth is instant. Arguments are part of the key, so pass everything the result depends on as an argument. A function that secretly reads a global variable or a widget value won't notice when that value changes, and will return stale results.
st.cache_resource: for connections and models
Some things shouldn't be copied: a database connection, a machine-learning model, an HTTP client. They're expensive to create and meant to be shared:
import streamlit as st
import sqlite3
@st.cache_resource
def get_connection():
conn = sqlite3.connect(":memory:", check_same_thread=False)
conn.execute("CREATE TABLE visits (city TEXT)")
return conn
conn = get_connection()
st.write("Same connection object on every rerun:", id(conn))
st.cache_data | st.cache_resource | |
|---|---|---|
| Use for | data: DataFrames, query results, API responses | resources: connections, models, clients |
| Returns | a fresh copy each call | the same object each call |
| Shared between users | yes (each gets a copy) | yes (the same object) |
Data you'll read and transform → cache_data. A thing you'll use (connect with, predict with) → cache_resource.
When cached data gets old
Cached results stay until the app restarts. For data that changes (today's orders), give the cache a lifetime:
import streamlit as st
import pandas as pd
@st.cache_data(ttl=600) # seconds: refresh at most every 10 minutes
def load_orders():
return pd.read_csv("sales.csv")
orders = load_orders()
if st.button("Refresh now"):
load_orders.clear() # forget cached results for this function
st.rerun()
st.write(f"{len(orders)} rows")
ttl=600(time to live): results older than 10 minutes are recomputed.load_orders.clear()empties this function's cache;st.cache_data.clear()empties all data caches.st.rerun()starts a new run immediately, which now loads fresh data.
(The simulator on this site doesn't expire ttl caches by time; the parameter is accepted so your code runs unchanged in real Streamlit.)
What not to cache
- Functions that are already fast: caching has overhead and hides bugs.
- Functions with side effects you need every time: sending an email, writing a log line. A cache hit skips the body, so the side effect silently doesn't happen.
- Results that depend on something not in the arguments (the current time, a global): stale results.
Why this matters for clean code
Caching rewards the clean-function habits from the Python track: a function whose result depends only on its arguments and that has no side effects is exactly what caches correctly. If a function is hard to cache, that's often a sign it's doing two jobs (loading and logging, computing and displaying). Split it, cache the pure part.
Recap
@st.cache_datacaches returned data per argument combination; callers get copies.@st.cache_resourcecaches shared objects (connections, models); callers get the same object.- Pass everything the result depends on as an argument; avoid hidden inputs.
ttl=limits age;fn.clear()orst.cache_data.clear()empties caches;st.rerun()reruns now.- Don't cache fast functions, side effects, or results with hidden inputs.