Streamlit is one of the fastest ways to build a data app with Python. You write a script, run it, and you instantly have a working web app. But there's a catch — Streamlit re-runs your entire script from top to bottom every time a user clicks a button or changes a slider.

That's fine for small apps. But once you start loading large datasets, running ML models, or making API calls, this re-run behavior makes your app feel painfully slow. This guide covers the three main tools that fix this: caching, session state, and smart code structure.

How Streamlit Works (and Why It Gets Slow)

Before we fix the problem, let's understand it. Every time a user interacts with your app — clicking a button, typing in a text box, moving a slider — Streamlit re-runs your whole Python script from line 1 to the end.

Python — what happens on every interaction
import streamlit as st import pandas as pd # This runs EVERY time the user does anything df = pd.read_csv('big_data.csv') # slow! loads every time option = st.selectbox('Choose a column', df.columns) st.line_chart(df[option])

In the example above, read_csv() runs every single time a user picks a new column. If the CSV is 100MB, your app freezes for a few seconds on every click. The fix is to tell Streamlit what to remember between runs.

ℹ️ Think of it like this: Streamlit is a chef who forgets everything between orders. Without caching, he re-reads the entire recipe book every time someone asks for a dish. Caching gives him a notepad to write down results he's already calculated.

Caching — The Biggest Speed Win

Caching means storing the result of a function so Streamlit can reuse it instead of running the function again. Streamlit gives you two decorators for this: st.cache_data and st.cache_resource.

st.cache_data — For Data

Use @st.cache_data for any function that loads or processes data — CSV files, API responses, database queries, computed results. Streamlit saves a copy of the output and returns it instantly next time.

Python — caching data with st.cache_data
import streamlit as st import pandas as pd @st.cache_data def load_data(): # This only runs ONCE, then result is cached df = pd.read_csv('big_data.csv') return df df = load_data() # instant after first load option = st.selectbox('Choose a column', df.columns) st.line_chart(df[option])

You can also set a time limit so the cache refreshes automatically. This is useful for live data that changes every few minutes:

Python — cache with a time limit (TTL)
@st.cache_data(ttl=300) # refresh every 5 minutes (300 seconds) def fetch_live_prices(): response = requests.get('https://api.prices.com/latest') return response.json() @st.cache_data(max_entries=10) # keep only 10 results in memory def process_user_data(user_id): return run_heavy_query(user_id)

st.cache_resource — For Shared Objects

Use @st.cache_resource for things that should be shared across all users — like a loaded ML model, a database connection, or an API client. Unlike cache_data, this does not make a copy. Everyone uses the same object.

Python — caching a model with st.cache_resource
import streamlit as st from transformers import pipeline @st.cache_resource def load_model(): # Loads once, shared by all users — saves huge memory return pipeline('sentiment-analysis') @st.cache_resource def get_db_connection(): return psycopg2.connect(DATABASE_URL) model = load_model() conn = get_db_connection() text = st.text_input('Enter text to analyse') if text: result = model(text) st.write(result)

Quick Comparison: cache_data vs cache_resource

Featurest.cache_datast.cache_resource
Best forDataFrames, API results, computed valuesML models, DB connections, API clients
Makes a copy?Yes — safe to mutateNo — shared object
Supports TTL?YesNo
Per-user or shared?Per unique inputShared across all users
⚠️ Watch out: cached functions must have hashable inputs. If you pass a list or dict as an argument, Streamlit may not be able to cache the result. Use simple types like strings, numbers, or tuples as function arguments when possible.

Session State — Remember Things Between Reruns

Caching stores the output of functions. But what about storing a user's choices, form inputs, or app state between reruns? That's what st.session_state is for.

Think of st.session_state as a dictionary that stays alive for the whole session. It doesn't reset when the script reruns — only when the user closes the browser tab.

Basic Usage

Python — session state basics
import streamlit as st if 'count' not in st.session_state: st.session_state.count = 0 st.write(f'Count: {st.session_state.count}') if st.button('Add 1'): st.session_state.count += 1 if st.button('Reset'): st.session_state.count = 0

Common Patterns

A very common use case is multi-step forms or wizards — keeping the user on step 2 even after the script reruns:

Python — multi-step form with session state
import streamlit as st if 'step' not in st.session_state: st.session_state.step = 1 if st.session_state.step == 1: st.header('Step 1: Enter your name') name = st.text_input('Name') if st.button('Next') and name: st.session_state.name = name st.session_state.step = 2 st.rerun() elif st.session_state.step == 2: st.header(f'Step 2: Welcome, {st.session_state.name}!') st.write('Continue filling in your details...')
Pro tip: use st.session_state to store expensive results that were computed based on user input — so if the user changes something unrelated, you don't have to recompute everything.

Code Structure — Load Less, Render Faster

Even with caching and session state, a badly structured app will still feel slow. Here are the key structural rules that make a big difference.

Lazy Loading — Only Load What You Need

Don't load all your data at the top of the file. Load it only when the user actually needs it. This means putting data-loading calls inside if blocks or inside tabs.

Python — lazy loading with tabs
import streamlit as st tab1, tab2, tab3 = st.tabs(['Overview', 'Deep Analysis', 'Raw Data']) with tab1: df_summary = load_summary() st.dataframe(df_summary) with tab2: # Heavy data — only loads when user clicks this tab df_full = load_full_dataset() run_analysis(df_full) with tab3: st.dataframe(load_raw())

st.fragment — Rerun Only Part of the Page

Added in Streamlit 1.33, @st.fragment is a game-changer. It lets you mark a function so that only that part of the page reruns when something inside it changes — the rest of the page stays still.

Python — st.fragment for partial reruns
import streamlit as st @st.fragment def chart_section(): col = st.selectbox('Pick a metric', ['Sales', 'Revenue', 'Users']) df = load_data() st.line_chart(df[col]) st.title('My Dashboard') load_expensive_header_data() chart_section()
ℹ️ When to use st.fragment: any time you have a widget whose changes don't need to affect the whole page. This is the most powerful performance tool in modern Streamlit.

UI Speed Tips — Make It Feel Fast

Performance isn't just about raw speed — it's about perceived speed. These small UI tricks make your app feel much snappier even when it's doing heavy work.

Python — spinners, progress bars and placeholders
import streamlit as st with st.spinner('Loading data...'): df = load_data() bar = st.progress(0) for i in range(100): process_chunk(i) bar.progress(i + 1) placeholder = st.empty() placeholder.text('Calculating...') result = heavy_calculation() placeholder.dataframe(result)
Quick wins checklist: use st.columns() to render charts side by side instead of stacked. Avoid st.write() for large objects — use the specific component like st.json() or st.dataframe() instead.

⚡ Key Takeaways
  • Streamlit reruns your whole script on every user interaction — this is the root cause of slowness.
  • Use @st.cache_data for loading data, API calls, and computed results — they get stored and reused instantly.
  • Use @st.cache_resource for shared objects like ML models and database connections — loaded once, used by everyone.
  • Add TTL (ttl=300) to cache_data when your data updates regularly.
  • Use st.session_state to remember user choices and computed results between reruns.
  • Use @st.fragment to make only part of the page rerun when a widget changes — huge speed gain.
  • Load data inside tabs or if blocks so it only runs when the user actually needs it.
  • Use st.spinner(), st.progress(), and st.empty() to make slow operations feel fast.