Immutable solution version
Fix Streamlit state wiped or duplicated: session_state vs reruns, cache_data vs cache_resource, widget-key collisions
Complete solution
Shows how to fix streamlit state wiped or duplicated: session_state vs reruns, cache_data vs cache_resource, widget-key collisions. Use it when you hit this exact problem. Skip it when your error message or symptom looks different.
TL;DR
For "StreamlitAPIException": use st.session\state for anything that must persist across reruns within one user's browser tab (one session). ``python import streamlit as st WRONG: count is re-created as 0 on every rerun, so it can never exceed 1. count = 0 if st.button("Increment"): count += 1 st.write("Count =", count) RIGHT: initialize once, guard with a membership test. if "count" not in st.session\state: st.session\state.count = 0 if st.button("Increment"): st.session\state.count += 1 st.write("Count =", st.session\_state.count) ` Rules from the docs: - Always initialize before use: if
StreamlitAPIExceptionWhen to use
You are seeing this: The concrete problem this skill solves: counters that never count, DataFrames that re-download on every click, and crashes with StreamlitDuplicateElementKey / DuplicateWidgetID, all caused by Streamlit's execution model: every widget interaction reruns... Use this skill when you run into "Fix Streamlit state wiped or duplicated: sessionstate vs reruns, cachedata vs cache_resource, widget-key collisions".
When not to use
If your error message or symptom does not match what is described above, this is probably not your fix. Search for your exact error text instead of forcing this one to fit.
Versions
No specific versions are mentioned in the source material, so treat the fix as generally applicable and check the examples against whatever you have installed.
Streamlit State Survival: sessionstate vs Reruns, cachedata vs cache_resource, and Widget-Key Collisions
The concrete problem this skill solves: counters that never count, DataFrames that re-download on every click, and crashes with StreamlitDuplicateElementKey / DuplicateWidgetID, all caused by Streamlit's execution model: every widget interaction reruns the entire script top to bottom in a blank slate. This skill gives you the rules for what survives a rerun, what doesn't, and where each kind of persistence belongs.
1. The rerun model: plain variables die, session_state survives
Every button click, slider move, or text input re-executes your script from the first line. Each run starts fresh: no local variables carry over. Use st.session_state for anything that must persist across reruns within one user's browser tab (one session).
import streamlit as st
# WRONG: `count` is re-created as 0 on every rerun, so it can never exceed 1.
count = 0
if st.button("Increment"):
count += 1
st.write("Count =", count)
# RIGHT: initialize once, guard with a membership test.
if "count" not in st.session_state:
st.session_state.count = 0
if st.button("Increment"):
st.session_state.count += 1
st.write("Count =", st.session_state.count)Rules from the docs:
- Always initialize before use: `if "key" not in st.sessionstate: st.sessionstate.key [your value] Reading an uninitialized key raises an exception.
- Both APIs work:
st.session_state.key(attribute) andst.session_state["key"](dict-style). - Session state is per browser tab (per WebSocket connection). Reloading the tab or navigating away resets it. The Streamlit server crashing wipes it. It is not a durable database.
- Session state persists across pages inside a multipage app.
- A callback (
on_click/on_change) executes before the script reruns. Read the widget's new value fromst.session_stateinside the callback (e.g.st.session_state.MY_KEY); do not pass the value throughargs/kwargs.
if "total" not in st.session_state:
st.session_state.total = 0
def add_five():
st.session_state.total += st.session_state.step # reads the LATEST widget value
st.number_input("Step", min_value=1, value=5, key [your value]
st.button("Add", on_click=add_five)
st.write("Total:", st.session_state.total)When you actually need a full extra rerun (e.g. a change must be visible in content drawn above the widget that caused it), call st.rerun(). It re-executes the whole script. Prefer callbacks and containers first, st.rerun() is slower, makes logic harder to follow, and can loop forever if placed behind a condition that stays true.
2. st.cachedata vs st.cacheresource: copy vs singleton
Caching solves a different problem than session state: expensive function calls that should run once, not per-user values. Both decorators cache globally (across all sessions and users), keyed on the function's input parameters plus its source code. Pick the wrong decorator and you get silent data corruption or unpicklable-object crashes.
@st.cache_data | @st.cache_resource | |
|---|---|---|
| For | Computations returning data | Global resources to create once |
| Examples | DataFrame from CSV, NumPy transforms, SQL query results, API responses | DB connection, ML model, file handle, thread pool |
| Return-value requirement | Must be pickle-serializable | Need not be serializable |
| What you get back each call | A fresh copy (pickle round-trip) | The same object (singleton) |
| Mutation safety | Safe: mutating the result can't corrupt the cache | Unsafe: mutations directly mutate the cached object, it must be thread-safe |
import streamlit as st
import pandas as pd
@st.cache_data # data serialized, copied on each call
def load_data(url):
return pd.read_csv(url)
df = load_data("https://example.com/dataset.csv")
st.dataframe(df)import streamlit as st
@st.cache_resource # resource: one global instance, returned as-is
def init_connection():
# st.secrets holds the connection parameters (host, database, user, password)
return DATABASE_DRIVER.connect(**st.secrets["postgres"])
conn = init_connection() # same connection object on every rerun, every sessionThe cache_data gotcha: because each call returns a deserialized copy, mutating the returned object is harmless to the cache, but it also means df is df is False across calls, and the pickle round-trip costs time on very large objects. The cache_resource gotcha is the mirror: there is exactly one global instance shared by all users and all sessions. If your code mutates it (appends to a list, changes model weights, advances a cursor), every other session sees the mutation. Return thread-safe, effectively read-only objects from cache_resource.
Decision rule from the docs: if you could store it in a database or on disk (str, int, float, array, DataFrame, list, dict), use st.cache_data. If it is a live, unserializable object you would never persist (connection, model, handle, thread), use st.cache_resource. When unsure, start with st.cache_data. To expire cached data, use ttl: @st.cache_data(ttl=3600) re-runs the function after one hour.
3. Widget keys: why duplicates crash your app
Every widget gets an internal key. If you don't pass key [your value] Streamlit generates one from the widget's **structure** (type + label + parameters). Two widgets with identical structure in the same script run produce the same generated key, and Streamlit raises StreamlitDuplicateElementKey (older versions: DuplicateWidgetID`):
StreamlitDuplicateElementKey - There are multiple elements with the same key [your value]Common triggers and fixes:
- Widgets in loops. A loop body executes multiple times with identical labels. Always include the loop variable in the key -
for i, filename in enumerate(filenames):
approved = st.checkbox(f"Approve {filename}", key [your value]- Widgets rendered in multiple branches/tabs. Streamlit renders all tab contents in the same pass, a key that is unique within one tab collides with the same key in another tab. Namespace keys by context: `key [your value]
- Copy-pasted widgets. Any two identical
st.button("OK")calls collide. Give each an explicit uniquekey.
Every widget with a key is automatically mirrored in st.session_state under that key, st.session_state.MY_KEY is the widget's current value. Hard rules on writing widget state:
- Never set a widget's key in
st.session_stateafter the widget has been instantiated in the same run. This raisesStreamlitAPIException. Set defaults before the widget line executes (or passvalue=to the widget instead). - Do not set the key via session state and pass
value=in the widget declaration at the same time, Streamlit warns on the first run; pick one. st.button,st.download_button, andst.file_uploadercannot have their state set via the session-state API at all. TheirTruestate is ephemeral, valid for exactly one run.
# WRONG: raises StreamlitAPIException
slider = st.slider("Choose a value", 1, 10, 5, key [your value]
st.session_state.my_slider = 7
# RIGHT: seed the default before the widget is created
if "my_slider" not in st.session_state:
st.session_state.my_slider = 7
slider = st.slider("Choose a value", 1, 10, 5, key [your value]4. Quick decision checklist
- Value must survive reruns for one user →
st.session_state, initialized with theif "k" not inguard. - Expensive pure computation or data load, same result for everyone →
@st.cache_data. - One shared live object for everyone (connection, model) →
@st.cache_resource; never mutate the returned object. - User-specific result that must not leak across sessions →
st.session_state, never the global caches. StreamlitDuplicateElementKeycrash → find the colliding `key [your value] (often in a loop or across tabs) and make it unique.- Display above the widget needs updating after a button click → restructure with a callback or container before reaching for
st.rerun(). - State vanished after browser reload → expected: session state lives only as long as the tab's WebSocket connection. Persist real data to a database or file.
Variants
Other phrasings of the same problem that show up in reports:
WRONG: raises StreamlitAPIException
# WRONG: raises StreamlitAPIException
slider = st.slider("Choose a value", 1, 10, 5, key [your value]
st.session_state.my_slider = 7
# RIGHT: seed the default before the widget is created
if "my_slider" not in st.session_state:
st.session_state.my_slider = 7
slider = st.slider("Choose a value", 1, 10, 5, key [your value]Evidence
Grounded in the official Streamlit docs: the session-state architecture guide (rerun model, per-tab sessions, initialization guard, button/fileuploader restrictions, WebSocket reset), the st.sessionstate API reference (callback execution order, StreamlitAPIException on post-instantiation writes, value-param warning), the caching overview (cachedata pickles and returns a copy; cacheresource returns the same global singleton and requires thread-safe return values; ttl expiry; data-vs-resource decision rule), and the st.rerun / st.fragment API references (callback-first preference, rerun semantics).
By @issueatlas · Version 2 · Exact version
Embed this skill
Add this card to a website or documentation page. It links to the current skill on Vectle.
<a href="https://vectle.com/skills/skl_so9y5ltsfwBNcn3wX1iWGw" style="display:inline-block;padding:12px 16px;border:1px solid #d8ddd3;border-radius:8px;color:#194d35;text-decoration:none;font:14px/1.5 system-ui,sans-serif">Fix Streamlit state wiped or duplicated: session_state vs reruns, cache_data vs cache_resource, widget-key collisions <span>— Vectle</span></a>