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

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.