Migrate to RunxBuild and earn up to $50 in hosting credit on your first deposit.

Calculate your savings
unxBuild

Streamlit Hosting: Deploying a Long-Lived Python Process

Sean

Platform Writer

Sep 07, 2026
8 min read

A Streamlit app is not a website. It is a Python process holding a WebSocket open to every connected browser, re-running your script from top to bottom whenever anyone clicks anything.

Streamlit Hosting: Deploying a Long-Lived Python Process

That one architectural fact explains everything that goes wrong when people deploy Streamlit for the first time. It is why static hosting cannot serve it, why the app forgets everything when it restarts, why memory climbs with concurrent users, and why it breaks behind a proxy that has not been told about WebSockets. Get the model right and the deployment is simple.

Table of contents

Why Streamlit does not fit a static host

When a browser opens a Streamlit app, the server starts a session, runs your script, and streams the resulting widget tree over a WebSocket. Every interaction sends a message back, the server re-runs the entire script with the new widget values, diffs the output, and pushes the changes down the same socket.

So there is no HTML file to serve. There is a Python process that must stay running, hold one session per connected user, and keep a socket open for as long as each tab is open. That rules out static hosting entirely and it rules out any deployment model that spins a process up per request.

It also means the unit of scale is concurrent sessions, not requests per second. Ten users clicking occasionally are ten live sessions with ten sets of in-memory state, whether or not anyone is actively doing anything.

The flags that matter in production

Streamlit’s defaults are tuned for running on a laptop. Four of them need changing before it goes anywhere near a server.

  • --server.port should read the port your platform assigns rather than the default 8501. Most hosts hand you a port in the environment and expect you to listen on it.
  • --server.address 0.0.0.0 binds to all interfaces. The default binds in a way that works locally and makes the app unreachable from outside a container. This is the single most common deployment failure.
  • --server.headless true stops Streamlit trying to open a browser and suppresses the first-run email prompt, which otherwise blocks startup waiting on stdin that will never arrive.
  • --browser.gatherUsageStats false turns off telemetry. Worth setting deliberately rather than inheriting.

There is also --server.enableXsrfProtection and --server.enableCORS, which people disable when things break behind a proxy. Resist that. If the app misbehaves through a reverse proxy, the problem is nearly always the WebSocket upgrade rather than XSRF, and turning off protections hides it rather than fixing it.

You can put all of this in .streamlit/config.toml instead of the command line, which keeps the run command short and the settings in version control.

A Dockerfile that works

Containerising Streamlit is straightforward and makes the deployment portable. The only subtlety is the health check, which needs Streamlit’s own endpoint rather than the app root.

FROM python:3.12-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 8501

HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health || exit 1

CMD ["streamlit", "run", "app.py", \
     "--server.port=8501", \
     "--server.address=0.0.0.0", \
     "--server.headless=true"]

/_stcore/health returns a plain ok and does not run your script, which is what you want from a health check. Pointing the check at the app root instead means every health probe starts a session and runs your data loading, which is both slow and misleading.

Pin your dependencies. Streamlit moves quickly and a rebuild months later against unpinned versions is a reliable way to discover that a widget API changed.

Session state, memory, and the scaling ceiling

Every connected browser gets its own st.session_state living in the server’s memory. Whatever you put there is per-user and disappears when the process restarts. That has three practical consequences.

  1. Memory scales with concurrent users. If each session holds a loaded dataframe, ten users hold ten copies. This is the usual reason a Streamlit app gets killed for running out of memory on a small instance.
  2. Restarts lose everything. A deploy, a crash, or an autoscaling event drops every session. If the app has a multi-step flow, users land back at step one.
  3. Horizontal scaling needs sticky sessions. Two replicas do not share session state, so a user whose WebSocket reconnects to the other replica gets a fresh session. Without session affinity at the load balancer, scaling out breaks the app rather than helping it.

The fix for the first is @st.cache_data and @st.cache_resource, which share loaded data and connections across sessions instead of duplicating them per user. Cache the dataframe once and ten sessions read the same copy. This is usually the difference between an app that needs 4GB and one that runs comfortably in 1GB.

The fix for the second is to put anything that must survive into a database rather than session state. The fix for the third is to scale up before scaling out, because a single larger instance avoids the affinity problem entirely and Streamlit apps are rarely CPU-bound.

WebSockets through a proxy

If you put nginx in front of Streamlit, the app loads and then hangs with a permanently running spinner. That is the WebSocket upgrade being dropped, and the proxy needs to be told to forward it.

location / {
    proxy_pass http://127.0.0.1:8501;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection upgrade;
    proxy_set_header Host $host;
    proxy_read_timeout 86400;
}

The long proxy_read_timeout matters as much as the upgrade headers. A Streamlit socket can sit idle while a user reads the page, and a 60-second default read timeout will close it underneath them, producing a connection-lost banner every minute.

If you are deploying to a platform rather than managing nginx yourself, check that it supports WebSockets on the route before you get far. Most application hosts do; static and edge-function hosts generally do not, which is another form of the same static-versus-process problem.

Sizing a Streamlit deployment

A rough model that holds up in practice. Start from the memory your data actually occupies once loaded, add per-session overhead, and leave headroom for the Python interpreter and your libraries.

  • A demo or internal tool with a handful of users and small data. Half a vCPU and 1GB is usually enough, provided you cache loaded data rather than loading it per session.
  • A dashboard over a few hundred megabytes of cached data. 1 vCPU and 2GB, because the cached copy plus per-session overhead adds up faster than people expect.
  • Anything doing model inference or heavy pandas work per interaction. 2 vCPU and 4GB and up, since re-running the script on every click makes CPU the constraint rather than memory.

Pair the app with a real database rather than reading files from the container’s disk. Container filesystems are ephemeral, so anything written there is gone on the next deploy, and a managed Postgres or MySQL instance gives the app somewhere durable to read from and write to.

On RunxBuild that shape is a Python service built from your GitHub repository, with the plan ladder starting at $4 for a Dev instance and $6 for Basic, and a managed database beside it. Autoscaling between a floor and a ceiling plan covers the case where usage is bursty rather than steady.

How this fits the rest of the stack

The thing worth internalising is that a Streamlit app costs what a small backend service costs, not what a static site costs, because that is what it is. Once you accept the process model, the sizing question is just how much memory your cached data needs and how many people click at once. The RunxBuild hosting calculator puts the service and the database beside each other as separate numbers, which is the honest way to look at it before you commit. Deploys build from the repository with runtime logs and rollback, so a dependency change that breaks startup is one click from being undone.

Useful related references:

FAQ

Can I host a Streamlit app on static hosting?

No. Streamlit runs a persistent Python process that holds a WebSocket open to each connected browser and re-runs your script on every interaction. Static hosting serves files and has nowhere for that process to live. You need an application host that runs a container or a long-lived process and supports WebSockets on the route.

Why does my Streamlit app hang on a spinner behind nginx?

The WebSocket upgrade is being dropped. Add proxy_http_version 1.1 and the Upgrade and Connection headers to the proxy location block, and raise proxy_read_timeout well above the default so an idle socket is not closed while a user reads the page.

How much RAM does a Streamlit app need?

It depends almost entirely on your data and how you cache it. A small internal tool runs in 1GB; a dashboard over a few hundred megabytes of cached data wants 2GB. The multiplier is concurrent sessions, so using st.cache_data to share loaded data across sessions instead of loading it per user is usually the biggest single saving.

Does session state survive a restart?

No. st.session_state lives in the server process memory and is lost on any restart, deploy, or crash, along with every connected session. Anything that must persist belongs in a database, not in session state.

Can I run multiple replicas of a Streamlit app?

Only with sticky sessions. Replicas do not share session state, so a reconnecting WebSocket that lands on a different replica gets a fresh session and the user loses their place. Scaling up to a larger single instance avoids the problem, and Streamlit apps are rarely CPU-bound enough to need scaling out.

#streamlit hosting#python#docker#websocket#deployment