Content bleeding in the deployed environment

Hello Streamlit community,

I’m experiencing a persistent content bleeding issue in my multi-page Streamlit app that
only occurs in the deployed environment (Kubernetes/Docker) but works perfectly in local
development.

The Problem

When navigating between pages with different amounts of content, old content from the
previous page remains visible underneath the current page:

  • Navigate from a large page (e.g., Executive Dashboard with multiple tabs and tables) to
    a smaller page (e.g., About page with just text)
  • Scroll down on the smaller page
  • Old content from the previous page is still visible below

This also affects sidebar content - filters and widgets from previous pages remain
visible.

Environment Details

Where it FAILS:

  • Kubernetes deployment (containerized)
  • Docker deployment
  • Streamlit Cloud deployment (if applicable)

Where it WORKS:

  • Local development with streamlit run
  • Works perfectly every time locally

What I’ve Tried

  1. Using st.empty() placeholders (recommended approach)

At the top of each page

sidebar_placeholder = st.sidebar.empty()
main_placeholder = st.empty()

Wrap all sidebar content

with sidebar_placeholder.container():
st.markdown(“## Filters”)

… all sidebar widgets

Wrap all main content

with main_placeholder.container():
st.title(“My Page”)

… all page content

Result: Works perfectly in local development, but content still bleeds in deployed
environment.

  1. Adding CSS to force layout consistency

st.markdown(“”"

.main .block-container { width: 100% !important; max-width: 100% !important; }

“”", unsafe_allow_html=True)

Result: Helps with layout compression but doesn’t fix content bleeding in deployment.

  1. Using st.rerun() and session state

Tried clearing session state and forcing reruns - still doesn’t clear old content in
deployed environment.

Code Structure

Page 1 (Large page with lots of content)

import streamlit as st

st.set_page_config(layout=“wide”)

sidebar_placeholder = st.sidebar.empty()
main_placeholder = st.empty()

with sidebar_placeholder.container():

Many filters and options

st.selectbox(“Filter 1”, options)
st.multiselect(“Filter 2”, options)

… more widgets

with main_placeholder.container():

Lots of content

tab1, tab2, tab3 = st.tabs([“Tab 1”, “Tab 2”, “Tab 3”])

Multiple dataframes, charts, metrics

Page 2 (Small page)

import streamlit as st

st.set_page_config(layout=“wide”)

sidebar_placeholder = st.sidebar.empty()
main_placeholder = st.empty()

with sidebar_placeholder.container():
st.markdown(“—”)
st.info(“Simple page”)

with main_placeholder.container():
st.title(“About”)
st.markdown(“Just some text here”)

Minimal content

Questions

  1. Why does this only happen in deployed environments? Local development works flawlessly
    with the exact same code.
  2. Is there a different approach for production deployments? Are there
    environment-specific Streamlit configurations needed?
  3. Could this be related to how Streamlit handles page state in containerized
    environments? Perhaps session management differs between local and deployed?
  4. Are there any Streamlit config.toml settings that might help with this issue in
    production?

Additional Context

  • The issue is consistent and reproducible in deployment
  • Refreshing the page (F5) clears the content temporarily
  • The problem gets worse with more page navigation
  • All pages use the same st.empty() placeholder pattern

Has anyone else experienced this? Any suggestions for making st.empty() placeholders work
consistently in deployed environments?

Thanks in advance!

Welcome to the Streamlit community and thanks for your detailed question! :waving_hand: This “content bleeding” or “ghosting” issue—where old page/sidebar content lingers after navigation—has been widely reported, especially in containerized or multi-replica deployments. The root cause is that Streamlit’s frontend sometimes doesn’t fully clear previous content when switching between pages or tabs, particularly when using st.empty() or containers. This is more pronounced in production environments due to differences in session handling, WebSocket routing, or load balancing, which can disrupt Streamlit’s state synchronization and cause stale UI elements to persist. Local development typically uses a single process and direct WebSocket connection, so the issue is masked there. See this discussion, this GitHub issue, and this deployment FAQ for more context.

Workarounds include: (1) Always define tabs/containers at the top of your script before any conditional logic; (2) Use unique keys for all widgets and containers; (3) For multipage apps, use st.navigation and st.Page, placing shared widgets in the entrypoint file to avoid state confusion (docs); (4) In Kubernetes/Docker, enable session affinity (“sticky sessions”) so all requests from a user go to the same backend instance (docs); (5) If using multiple containers, explicitly empty all placeholders on page change; (6) For file uploads and sidebar widgets, ensure session cookies and secrets are consistent across replicas (docs). There are no config.toml settings that directly fix this, but session affinity and consistent secrets can help. If you want a step-by-step code refactor or more technical details, just let me know! And if anyone in the community has a bulletproof fix, please jump in and share your insights. :rocket:

Sources:

Can you share a minimal, executable reproduction (formatted as one code block per file, maybe just cleaning up the formatting in your example above)?

You should not need an st.empty or container wrapper on your pages. I would get rid of that. If anything, I think that might be more likely to introduce unexpected behavior from its subtlties.

Are you using the pagesdirectory or st.navigation? Do you have any slow processes that might be running slower in a deployment than in a local environment? Is the bleeding content faded (what we call stale) or does it appear as if it’s active and current? When you are seeing the extra content, do you see the running animation still going in the upper-right corner?