Python/Shiny Long Running Dashboards - Memory Leaks In Server and Client

One of the use cases for Shiny dashboards in our manufacturing plant is for long-running dashboards, where a series of charts on a Shiny dashboard update on a timed interval. The client is a Chromebox that loads the dashboard from a URL to content hosted on our Posit Connect server.

Unfortunately, these deployments are failing due to severe memory leaks on the server and client side.

I have a simple sample app that creates a panel of six plotly charts that display some temperature data in the local browsers time zone. These charts update on a timed interval with a dataset that is provided by an internal API. For my sample program, we just load from a local JSON file.

This application will result in the Microsoft Edge browser tab that hosts it accumulating 4GB of memory usage in a couple of hours and ultimately crash the browser. The server side memory creeps up from 150MB to over 1GB in the same time period.

It doesn't seem like the memory is being freed properly in these Shiny applications. I'm not sure if we can do anything differently. We previously used pure Javascript for this kind of thing and never saw these kinds of issues with memory bloat. Is there a better way to handle this in Shiny for Python?

Appreciate any advice.
Thanks,
Matt

app.py

from pathlib import Path
from shiny import App, ui, reactive, session
import modTHQA

# Define the UI
app_ui = ui.page_fluid(
    ui.tags.script("""
            Shiny.initializedPromise.then(function() {
                var timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
                Shiny.setInputValue('browser_timezone', timezone, {priority: "event"});
                //Shiny.shinyapp.$allowReconnect = "force"
            });
    """),
    ui.tags.style("""
        body {
            background-color: #000000;
            color: #ffffff;
        }
        .panel {
            background-color: #3e3e3e;
            border-color: #555555;
        }
    """),
    ui.row(
        ui.column(6,
            modTHQA.panel("template1"),
        ),
        ui.column(6,
            modTHQA.panel("template2"),
        )
    ),
    ui.row(
         ui.column(6,
            modTHQA.panel("template3"),
        ),
        ui.column(6,
            modTHQA.panel("template4"),
        )
    ),
    ui.row(
         ui.column(6,
            modTHQA.panel("template5"),
        ),
        ui.column(6,
            modTHQA.panel("template6"),
        )
    ),
)

# Define the server logic
def server(input, output, session):

    @reactive.Calc
    def get_timezone():
        timezone = input.browser_timezone()
        return timezone
       
    modTHQA.server("template1", get_timezone)
    modTHQA.server("template2", get_timezone)
    modTHQA.server("template3", get_timezone)
    modTHQA.server("template4", get_timezone)
    modTHQA.server("template5", get_timezone)
    modTHQA.server("template6", get_timezone)

# Create the app
app_dir = Path(__file__).parent
app = App(app_ui, server, static_assets=app_dir / "www", debug=False) 

# Run the app
if __name__ == "__main__":
    app.run()

modTHQA.py

from pathlib import Path
from shiny import module, render, ui, reactive, session
from shinywidgets import output_widget, render_widget, render_plotly
import numpy as np
import pandas as pd
import requests
from datetime import datetime
import plotly.express as px

@module.ui
def panel():
    return ui.div(
        ui.row(
            "Temperature",
            ui.output_text("debug"),
            ui.column(12, output_widget("th",height="300px")),
        ),
    )

@module.server
def server(input, output, session, get_timezone):
   
    @reactive.Calc
    def fetch():
        df = pd.DataFrame()
        reactive.invalidate_later(10)
        timezone = get_timezone()
        if timezone:
            json_path = Path(__file__).parent / "www" / "data.json"
            df = pd.read_json(json_path)
            df["time"] = pd.to_datetime(df["DateTime"])
            df["localtime"] = df["time"].dt.tz_convert(timezone)
        return df

    @render.text
    def debug():
        return f"Timezone: {get_timezone()}"

    @output
    @render_plotly
    def th():
        data = fetch()
        fig = px.line(data, x="localtime", y="SPA_T3610_3_TEMP", title="Temperature")
        fig.update_layout(
                xaxis_title="Timestamp",
                yaxis_title="Temperature (F)",
                xaxis=dict(
                    tickformat="%Y-%m-%d %H:%M:%S",
                    tickangle=45
                )
        )
        return fig

requirements.txt

shiny==1.2.0
shinywidgets==0.3.4
numpy==2.1.3
pandas==2.2.3
requests==2.32.3
plotly==5.24.1
1 Like

Looks like the data.json file is too large to post, but I can send manually if needed.

@mwitzman To finish the reprex, can you post your json data? Thank you

trunctated segment of json data

data.json

[{"DateTime":"2024-10-28T17:43:54.000Z","SPA_DMT342A_DP":-65.93443298339844,"SPA_DMT342A_PPM":0.561188280582428,"SPA_T3610_TEMP":66.9,"SPA_T3610_ABS":7.5,"SPA_T3610_RH":44.8,"SPA_T3610_DP":44.8,"SPA_T3610_3_TEMP":66.6,"SPA_T3610_3_ABS":7.7,"SPA_T3610_3_RH":46.5,"SPA_T3610_3_DP":45.5},{"DateTime":"2024-10-28T18:13:54.000Z","SPA_DMT342A_DP":-66.52381896972656,"SPA_DMT342A_PPM":0.5158581733703613,"SPA_T3610_TEMP":66.9,"SPA_T3610_ABS":7.5,"SPA_T3610_RH":44.9,"SPA_T3610_DP":44.8,"SPA_T3610_3_TEMP":67,"SPA_T3610_3_ABS":7.7,"SPA_T3610_3_RH":45.7,"SPA_T3610_3_DP":45.400000000000006},{"DateTime":"2024-10-28T18:43:54.000Z","SPA_DMT342A_DP":-65.59101867675781,"SPA_DMT342A_PPM":0.589284360408783,"SPA_T3610_TEMP":67,"SPA_T3610_ABS":7.5,"SPA_T3610_RH":44.5,"SPA_T3610_DP":44.7,"SPA_T3610_3_TEMP":67.9,"SPA_T3610_3_ABS":7.7,"SPA_T3610_3_RH":44.8,"SPA_T3610_3_DP":45.599999999999994},{"DateTime":"2024-10-28T19:13:54.000Z","SPA_DMT342A_DP":-65.81533813476562,"SPA_DMT342A_PPM":0.5707865357398987,"SPA_T3610_TEMP":67,"SPA_T3610_ABS":7.4,"SPA_T3610_RH":44.3,"SPA_T3610_DP":44.599999999999994,"SPA_T3610_3_TEMP":67.7,"SPA_T3610_3_ABS":7.7,"SPA_T3610_3_RH":44.8,"SPA_T3610_3_DP":45.5},{"DateTime":"2024-10-28T19:43:54.000Z","SPA_DMT342A_DP":-65.91316223144531,"SPA_DMT342A_PPM":0.5628912448883057,"SPA_T3610_TEMP":67.2,"SPA_T3610_ABS":7.5,"SPA_T3610_RH":44.5,"SPA_T3610_DP":44.900000000000006,"SPA_T3610_3_TEMP":67.4,"SPA_T3610_3_ABS":7.6,"SPA_T3610_3_RH":44.9,"SPA_T3610_3_DP":45.3},{"DateTime":"2024-10-28T20:13:54.000Z","SPA_DMT342A_DP":-66.07929992675781,"SPA_DMT342A_PPM":0.5497143864631653,"SPA_T3610_TEMP":67.1,"SPA_T3610_ABS":7.4,"SPA_T3610_RH":44.1,"SPA_T3610_DP":44.599999999999994,"SPA_T3610_3_TEMP":67.2,"SPA_T3610_3_ABS":7.5,"SPA_T3610_3_RH":44.3,"SPA_T3610_3_DP":44.8},{"DateTime":"2024-10-28T20:43:54.000Z","SPA_DMT342A_DP":-65.29019165039062,"SPA_DMT342A_PPM":0.6149653792381287,"SPA_T3610_TEMP":67.2,"SPA_T3610_ABS":7.4,"SPA_T3610_RH":43.7,"SPA_T3610_DP":44.400000000000006,"SPA_T3610_3_TEMP":67,"SPA_T3610_3_ABS":7.7,"SPA_T3610_3_RH":45.9,"SPA_T3610_3_DP":45.5},{"DateTime":"2024-10-28T21:13:54.000Z","SPA_DMT342A_DP":-65.22857666015625,"SPA_DMT342A_PPM":0.6203572750091553,"SPA_T3610_TEMP":67.2,"SPA_T3610_ABS":7.5,"SPA_T3610_RH":44.4,"SPA_T3610_DP":44.8,"SPA_T3610_3_TEMP":67.1,"SPA_T3610_3_ABS":7.7,"SPA_T3610_3_RH":45.8,"SPA_T3610_3_DP":45.599999999999994},{"DateTime":"2024-10-28T21:43:54.000Z","SPA_DMT342A_DP":-65.91029357910156,"SPA_DMT342A_PPM":0.5631204843521118,"SPA_T3610_TEMP":67.5,"SPA_T3610_ABS":7.5,"SPA_T3610_RH":43.7,"SPA_T3610_DP":44.7,"SPA_T3610_3_TEMP":67.1,"SPA_T3610_3_ABS":7.7,"SPA_T3610_3_RH":45.8,"SPA_T3610_3_DP":45.599999999999994},{"DateTime":"2024-10-28T22:13:54.000Z","SPA_DMT342A_DP":-66.23077392578125,"SPA_DMT342A_PPM":0.537948727607727,"SPA_T3610_TEMP":67.6,"SPA_T3610_ABS":7.6,"SPA_T3610_RH":44.4,"SPA_T3610_DP":45.2,"SPA_T3610_3_TEMP":67.2,"SPA_T3610_3_ABS":8,"SPA_T3610_3_RH":47.4,"SPA_T3610_3_DP":46.599999999999994},{"DateTime":"2024-10-28T22:43:54.000Z","SPA_DMT342A_DP":-65.5190200805664,"SPA_DMT342A_PPM":0.5953372120857239,"SPA_T3610_TEMP":67.3,"SPA_T3610_ABS":7.7,"SPA_T3610_RH":45.3,"SPA_T3610_DP":45.400000000000006,"SPA_T3610_3_TEMP":67.4,"SPA_T3610_3_ABS":7.9,"SPA_T3610_3_RH":46.4,"SPA_T3610_3_DP":46.099999999999994},{"DateTime":"2024-10-28T23:13:54.000Z","SPA_DMT342A_DP":-65.65274047851562,"SPA_DMT342A_PPM":0.5841381549835205,"SPA_T3610_TEMP":67.7,"SPA_T3610_ABS":7.7,"SPA_T3610_RH":45,"SPA_T3610_DP":45.599999999999994,"SPA_T3610_3_TEMP":67.7,"SPA_T3610_3_ABS":7.8,"SPA_T3610_3_RH":45.3,"SPA_T3610_3_DP":45.8},{"DateTime":"2024-10-28T23:43:54.000Z","SPA_DMT342A_DP":-65.65118408203125,"SPA_DMT342A_PPM":0.5842678546905518,"SPA_T3610_TEMP":66.2,"SPA_T3610_ABS":7.7,"SPA_T3610_RH":47.1,"SPA_T3610_DP":45.5,"SPA_T3610_3_TEMP":67.7,"SPA_T3610_3_ABS":7.6,"SPA_T3610_3_RH":44.5,"SPA_T3610_3_DP":45.3},{"DateTime":"2024-10-29T00:13:54.000Z","SPA_DMT342A_DP":-66.19075012207031,"SPA_DMT342A_PPM":0.5410367250442505,"SPA_T3610_TEMP":65.9,"SPA_T3610_ABS":7.7,"SPA_T3610_RH":47.5,"SPA_T3610_DP":45.400000000000006,"SPA_T3610_3_TEMP":67.7,"SPA_T3610_3_ABS":7.5,"SPA_T3610_3_RH":43.6,"SPA_T3610_3_DP":44.8},{"DateTime":"2024-10-29T00:43:54.000Z","SPA_DMT342A_DP":-65.3711929321289,"SPA_DMT342A_PPM":0.6079509854316711,"SPA_T3610_TEMP":67.1,"SPA_T3610_ABS":7.6,"SPA_T3610_RH":45.1,"SPA_T3610_DP":45.2,"SPA_T3610_3_TEMP":67.6,"SPA_T3610_3_ABS":7.2,"SPA_T3610_3_RH":42.3,"SPA_T3610_3_DP":43.9},{"DateTime":"2024-10-29T01:13:54.000Z","SPA_DMT342A_DP":-65.41841125488281,"SPA_DMT342A_PPM":0.6038962006568909,"SPA_T3610_TEMP":68.1,"SPA_T3610_ABS":7.5,"SPA_T3610_RH":42.9,"SPA_T3610_DP":44.8,"SPA_T3610_3_TEMP":68,"SPA_T3610_3_ABS":8,"SPA_T3610_3_RH":46.2,"SPA_T3610_3_DP":46.599999999999994},{"DateTime":"2024-10-29T01:43:54.000Z","SPA_DMT342A_DP":-65.55091857910156,"SPA_DMT342A_PPM":0.5926482081413269,"SPA_T3610_TEMP":67.3,"SPA_T3610_ABS":7.3,"SPA_T3610_RH":43.3,"SPA_T3610_DP":44.3,"SPA_T3610_3_TEMP":69.2,"SPA_T3610_3_ABS":7.7,"SPA_T3610_3_RH":42.7,"SPA_T3610_3_DP":45.599999999999994},{"DateTime":"2024-10-29T02:13:54.000Z","SPA_DMT342A_DP":-65.79646301269531,"SPA_DMT342A_PPM":0.5723211765289307,"SPA_T3610_TEMP":66.5,"SPA_T3610_ABS":7.2,"SPA_T3610_RH":43.9,"SPA_T3610_DP":43.9,"SPA_T3610_3_TEMP":69.5,"SPA_T3610_3_ABS":7.4,"SPA_T3610_3_RH":40.9,"SPA_T3610_3_DP":44.8},{"DateTime":"2024-10-29T02:43:54.000Z","SPA_DMT342A_DP":-65.90019226074219,"SPA_DMT342A_PPM":0.5639327168464661,"SPA_T3610_TEMP":66.1,"SPA_T3610_ABS":7.1,"SPA_T3610_RH":43.7,"SPA_T3610_DP":43.4,"SPA_T3610_3_TEMP":69.7,"SPA_T3610_3_ABS":7.4,"SPA_T3610_3_RH":40.2,"SPA_T3610_3_DP":44.5},{"DateTime":"2024-10-29T03:13:54.000Z","SPA_DMT342A_DP":-65.57157897949219,"SPA_DMT342A_PPM":0.590912938117981,"SPA_T3610_TEMP":65.9,"SPA_T3610_ABS":7,"SPA_T3610_RH":43.3,"SPA_T3610_DP":43,"SPA_T3610_3_TEMP":70,"SPA_T3610_3_ABS":7.4,"SPA_T3610_3_RH":40.1,"SPA_T3610_3_DP":44.7},{"DateTime":"2024-10-29T03:43:54.000Z","SPA_DMT342A_DP":-65.64445495605469,"SPA_DMT342A_PPM":0.5848302841186523,"SPA_T3610_TEMP":66,"SPA_T3610_ABS":7,"SPA_T3610_RH":42.9,"SPA_T3610_DP":42.8,"SPA_T3610_3_TEMP":70,"SPA_T3610_3_ABS":7.4,"SPA_T3610_3_RH":39.9,"SPA_T3610_3_DP":44.5},{"DateTime":"2024-10-29T04:13:54.000Z","SPA_DMT342A_DP":-65.83259582519531,"SPA_DMT342A_PPM":0.5693877935409546,"SPA_T3610_TEMP":67.1,"SPA_T3610_ABS":7,"SPA_T3610_RH":41.6,"SPA_T3610_DP":43,"SPA_T3610_3_TEMP":70,"SPA_T3610_3_ABS":7.4,"SPA_T3610_3_RH":40,"SPA_T3610_3_DP":44.599999999999994},{"DateTime":"2024-10-29T04:43:54.000Z","SPA_DMT342A_DP":-65.68814086914062,"SPA_DMT342A_PPM":0.5812084674835205,"SPA_T3610_TEMP":67.9,"SPA_T3610_ABS":6.9,"SPA_T3610_RH":40,"SPA_T3610_DP":42.8,"SPA_T3610_3_TEMP":70.1,"SPA_T3610_3_ABS":7.4,"SPA_T3610_3_RH":39.8,"SPA_T3610_3_DP":44.599999999999994},{"DateTime":"2024-10-29T05:13:54.000Z","SPA_DMT342A_DP":-65.77427673339844,"SPA_DMT342A_PPM":0.574130654335022,"SPA_T3610_TEMP":68,"SPA_T3610_ABS":6.9,"SPA_T3610_RH":39.6,"SPA_T3610_DP":42.6,"SPA_T3610_3_TEMP":70.1,"SPA_T3610_3_ABS":7.4,"SPA_T3610_3_RH":39.8,"SPA_T3610_3_DP":44.599999999999994}]

@mwitzman Sorry to point the finger somewhere else, but something odd is going on with plotly and jupiter widgets. :cry:

Link to shinylive.io app whose dependencies are not locked, but the latest supported versions

I shortened the timeout time on the fetching of data to produce more errors. (Note: if you set it to 0.5, you hit ~200k console errors in 1 minute! Never seen that before!)

The console will rapidly produce errors. Trying to run a memory profile over the site, I can verify that some memory isn't being released. You can see the stair step of the horizontal lines in the top right corner. (This is the memory leak)

Inspecting further, the top memory leak is from the function f from index.js of jupiter-widgets:

Changing the plotly output to a ui.output_table() causes the memory leak to disappear. Link to table app on shinylive


With the stair step being removed after changing the output type, I am led to the conclusion that the leak is caused by plotly / jupiter-wdigets.

1 Like

I'll look more into whether shinywidgets can do anything to prevent this issue, but in the meantime, I'd also recommend switching from reactive.invalidate_later() to reactive.file_reader(). As a result, the plots will only re-render when the file actually changes, which should help to limit the amount of leakage.

1 Like

Carson,

Thank you for taking a look at this.

In a real application, I am making web requests through an API for data, not reading from static files so the reactive.file_reader() won't resolve this particular issue.

Given that these jupiter widgets and plotly charts are pretty fundamental to a well functioning Python/Shiny application, perhaps we can collaborate to help communicate the memory management issues to the proper maintainers. Jasmine at Posit has my contact details.

These memory leaks took out our Posit Connect server this week (out of memory), so I would imagine that Posit would want to keep pretty close tabs on this one.

Thanks,
Matt

an observation :
adding explicit (constant) width and height to the plotly.express charting code removed the js/side errors in the console log for me.

fig = px.line(
            data,
            x="localtime",
            y="SPA_T3610_3_TEMP",
            title="Temperature",
            height=300,
            width=300
        )

Also some googling about showed me users of angular/plotly had same JS error , and their repo was patched with a fix for that , related to resizing (many years ago)

1 Like

These memory leaks took out our Posit Connect server this week (out of memory), so I would imagine that Posit would want to keep pretty close tabs on this one.

Oh no, sorry about that! Now that I've had some time to review, I think I've identified the source of the problem, and I might be able to address it in {shinywidgets}. Stay tuned, hoping to have a fix (or at least a workaround) you can try out tomorrow.

Also, FWIW, I'm pretty sure the JS resize errors that @barret mentioned don't have anything to do with the server-side leak.

Hi again @mwitzman, thanks for your patience, this has been quite a tricky issue to get to the bottom of!

If you're in need of an urgent fix, I have a proposal for a fix (which we'll be reviewing and merging hopefully next week). In the meantime, you can try it out by changing this line in your requirements.txt:

shinywidgets==0.3.4

to

git+https://github.com/posit-dev/py-shinywidgets.git@cleanup-orphaned-widgets

And please let me know if you come across any futher issues, thanks.

1 Like

Thanks Carson, I helped Matt test out the shinywidgets update.

The client-side memory (Windows/Chrome) is relatively stable, but the server-side memory still grows quickly.

Over 3 hours:
client side: ~470MB to ~550MB
server side: 250MB to 1.7GB

-Austin

Whoops, sorry about that! A commit I pushed earlier today did in fact introduce an issue, but it should be resolved now. Feel free to try reinstalling from that branch again and let me know if you find further issues, thanks.

Ok, we've merged a fix into the main branch now. Please update your requirements.txt to the following:

git+https://github.com/posit-dev/py-shinywidgets.git

And, again, please let me know how it goes.

I will test again with the current main branch overnight.

I was able to redeploy yesterday with reference to this commit: git+https://github.com/posit-dev/py-shinywidgets.git@6fb9d7c0e57637cbbbee6e906f582effde7cb8bf

Unfortunately the memory leak remained - over 24 hours:
client-side memory: 1.5GB
server memory: 4.8GB

I'll try again with the current main branch now.

Huh, weird, I can't seem to replicate. Are you deploying the original example verbatim? How are you measuring memory usage? What version of python?

For sake of comparison, it might also be useful to have memory usage with the pypi release over the same period of time.

One more thing -- it'd also be good to verify the version number. It should be:

>>> import shinywidgets
>>> shinywidgets.__version__
'0.3.4.9001'

Unfortunately something interrupted the experiment - I restarted now.
We are monitoring via Posit Connect Admin panel

From the server deployment last night: Python 3.11.4; shinywidgets@fb077...
2024/11/20 20:14:06.982394831 Building environment using Python 3.11.4 (main, Jun 6 2023, 23:28:13) [GCC 8.5.0 20210514 (Red Hat 8.5.0-16)] at /opt/python/3.11.4/bin/python3.11 2024/11/20 20:14:06.982674551 Using cached environment: njsqApkCTNBaHmrMAlffJw 2024/11/20 20:14:07.384830565 Packages in the environment: aiofiles==24.1.0, anyio==4.6.2.post1, appdirs==1.4.4, asgiref==3.8.1, asttokens==2.4.1, certifi==2024.8.30, charset-normalizer==3.4.0, click==8.1.7, comm==0.2.2, decorator==5.1.1, executing==2.1.0, h11==0.14.0, htmltools==0.6.0, idna==3.10, ipython==8.18.0, ipywidgets==8.1.5, jedi==0.19.2, jupyter_core==5.7.2, jupyterlab_widgets==3.0.13, linkify-it-py==2.0.3, markdown-it-py==3.0.0, matplotlib-inline==0.1.7, mdit-py-plugins==0.4.2, mdurl==0.1.2, narwhals==1.14.1, numpy==2.1.3, orjson==3.10.11, packaging==24.2, pandas==2.2.3, parso==0.8.4, pexpect==4.9.0, platformdirs==4.3.6, plotly==5.24.1, prompt-toolkit==3.0.36, ptyprocess==0.7.0, pure_eval==0.2.3, Pygments==2.18.0, python-dateutil==2.9.0.post0, python-multipart==0.0.17, pytz==2024.2, questionary==2.0.1, requests==2.32.3, shiny==1.2.0, shinywidgets @ git+https://github.com/posit-dev/py-shinywidgets.git@fb077be0da473c8c29b9e204ca7bcfd22bcc034a

Ah ok, thanks. Sorry to do this, but you might want to try again with the latest commit d4bb85, which should make sure you aren't getting some cached version of shinywidgets

Thanks Carson.

My apologies, I just realized I may have tested an older deployment on accident. I am testing the d4bb85 commit now.

I tested the same app unchanged as above with shinywidgets commit d4bb85 over ~24 hours.

The good news - the server memory is stable at ~333MB.

The bad news, the client is at 4GB.