In Streamlit, what is shared between two users of the same deployed app, and what is not?
answer
- one process, many websocket sessions
- the isolation boundary is the script run
- globals and caches sit below that boundary
- one object handed to everyone can be mutated
- replicas share nothing with each other
basics
~20 sEach browser session gets its own script run and its own st.session_state. The Python process, module-level globals and everything in st.cache_data or st.cache_resource are shared by all sessions on that server, so shared mutable state is global by accident.
solid answer
~50 sA deployed Streamlit app is one Python process serving many websocket sessions. Per session you get an independent script execution — running in its own thread — and an independent `st.session_state`. Shared across every session on that process: imported modules and module-level globals, and both caches. `st.cache_resource` hands the *same* object to everyone, so a connection pool or model is deliberately shared and must be thread-safe; `st.cache_data` hands out copies but the cached entry itself is still shared, which is why caching user-scoped rows without the user in the key is a data-leak shape. Because it is one process, a blocking call or a CPU-bound loop in one session competes with every other user under the GIL. And if you scale to several replicas, nothing is shared between them: caches warm independently, and session state dies if a viewer reconnects to a different replica.
code
python · 11 linesimport streamlit as st
HITS = [] # ONE list for the whole app, all users
@st.cache_resource
def get_client(): # SAME object handed to every session
return SomeApiClient() # must be thread-safe
@st.cache_data(ttl=300)
def load_orders(user_id: str, day: str): # identity IS part of the key
return query(user_id, day)go deeper
Know the basic split: session state belongs to one browser session, caches are shared by everyone using the app. That alone prevents the worst beginner mistakes.
Explain that one process serves all sessions with a thread per script run, and why st.cache_resource objects therefore have to be thread-safe while st.cache_data hands out copies.
Demonstrate that you have operated one: pool sizing, TTLs, identity in the cache key, moving heavy work off the interaction path, and diagnosing an app that is slow only when several people use it.
Own the deployment posture — authentication in front, per-user credentials to the warehouse, replica count and what shared-nothing caching costs, and whether this workload belongs in an app process at all.
## The runtime shape Streamlit serves an app from a single Python process. Each browser tab that connects opens a websocket and becomes a **session**. When that session interacts, Streamlit runs the script for that session — in its own thread inside the shared process. So the isolation boundary is *the script run and its session state*, and everything below that boundary is common ground. ## Per session - **The script run.** Local variables, control flow, the widget values sent from that browser. - **`st.session_state`.** Independent per session, empty at first connect, destroyed on disconnect. - **The rendered page.** One session's rerun does not touch another's screen. ## Shared by every session on the process - **Module-level globals.** A `RESULTS = []` at module scope is one list for the whole app. It looks like ordinary Python and behaves like a global variable in a multi-user server, because that is what it is. - **`st.cache_resource` entries.** Every session receives the identical object. This is the feature — one connection pool, one loaded model — and the hazard: anything mutable stored in it is app-wide, and it must tolerate concurrent use from several threads. - **`st.cache_data` entries.** Callers get copies, but the *entry* is shared. If the key does not distinguish users, user A's data is served to user B. - **Configuration and secrets.** Loaded once, server-side; the browser never sees them unless you render them. ## The concurrency consequences One process means the GIL. A session that runs a long CPU-bound loop in pure Python holds it and everyone else's app feels sluggish; a session that blocks on a slow query at least releases the GIL during I/O but still occupies a thread and, more importantly, a slot in whatever connection pool you configured. Two practical rules follow: keep per-interaction work small and cached, and size connection pools for concurrent sessions rather than for one developer's laptop. Thread-safety is not theoretical either. A cached HTTP client is usually fine; a cached object holding a mutable cursor, a partially built DataFrame or an unguarded counter is a race waiting to happen. If shared mutable state is genuinely required, guard it with a lock, or move it out of the process to a database or Redis where the concurrency semantics are somebody else's solved problem. ## The security consequence Streamlit does not, by itself, give you a per-user data boundary. There is no built-in row-level security model and, in a plain deployment, no notion of who is looking. Anything you do about identity — a reverse proxy that injects a header, an identity provider in front of the app, a per-user credential to the warehouse — you wire yourself, and you must then make sure that identity reaches the *cache key* and the *query*, not just the greeting at the top of the page. The classic incident is a well-meaning `@st.cache_data` on `load_my_orders(start, end)` that quietly serves the first caller's orders to everyone with the same date range. ## Scaling out Horizontal scaling puts several independent processes behind a load balancer. Each has its own caches and its own session states. Consequences to plan for: - **Warm-up multiplies.** Every replica pays first-load cost for models and big queries. - **Cache clearing is local.** A "refresh" button clears the replica that handled the click. - **Sessions are pinned by the websocket** for their lifetime, but a reconnect can land elsewhere and starts empty, so any state a user would hate to lose must be persisted externally, not held in `st.session_state`. - **Deploys are disruptive.** Restarting the process drops every session and every cache; users see the app reset. Communicate that, or schedule it. ## How to design with the grain Treat the app as a stateless renderer over shared, cached, read-mostly resources: connections and models in `st.cache_resource`; query results in `st.cache_data` with a TTL and, where user-specific, the identity in the key; small control state in `st.session_state`; anything durable in a real store. Where a workload is heavy or long-running, push it out — a job queue, a warehouse task, an async worker — and let the app poll for a result instead of holding a thread for a minute. Those choices are exactly what a senior interviewer is probing: not whether you can write `st.slider`, but whether you have run one of these apps for more than one person at a time.
- A cached function returns rows filtered for the logged-in user. What is the minimum fix?Put the user identity into the cache key as a real (non-underscore) argument, so each user's entry is distinct, and bound it with max_entries and a TTL. Better still, let the database enforce the filter using a per-user credential or a policy, so a caching mistake cannot become a disclosure.
- How would you keep one user's heavy computation from slowing everyone down?Move it off the request path: submit it to a job queue, warehouse task or worker, store the result, and have the app poll or cache the finished output. Failing that, cap concurrency with a semaphore and make the app show a queued state. Long CPU-bound Python inside a session competes with every other user in the same process.
- What happens to a user's session when you redeploy the app?The process restarts, so every session state and every cache entry is gone and connected users see the app reload from scratch. Persist anything that matters externally, and prefer deploying at quiet times or with a short banner; there is no session migration to lean on.
saying these in an interview costs you the question
- Assumes each user gets their own Python process
- Stores shared state in a module-level global without a lock
- Caches user-scoped data without the user in the key
- Thinks session state survives a redeploy or replica switch
- Runs long CPU-bound work in the script and calls it fine