Hey everyone!
If you’ve ever tried to put an LLM in a Dash app, you know the pain. The model writes its answer token by token, but a callback returns one answer, all at once. So your user sits there watching a spinner until the whole thing is done. Same story for any job that runs for a minute and has progress it could be reporting.
Dash 4.5.0 fixes that. Callbacks can now stream: every yield reaches the browser as soon as it’s ready.
Getting that to work across tabs, workers and servers meant building something Dash never had before: shared storage, a way for every process behind an app to share state. And while we were in there, we added React 19 support and finally figured out why apps had been getting slower since 4.2.
This is the biggest Dash release since 4.0. Nineteen people from the community shipped features and fixes in it, and it took three release candidates (and a few changes of mind) to get here. Here’s what’s new, who built it, and what we learned along the way.
Streaming callbacks and shared storage
A streaming callback is just an async def generator. Each yield goes straight to the browser, and if you yield a dash.Patch, it gets applied incrementally. So a long job can fill a table row by row, or grow a chart point by point:
@callback(
Output("log", "children"),
Input("run", "n_clicks"),
prevent_initial_call=True,
)
async def run_job(_):
yield []
async for line in job_output():
patch = Patch()
patch.append(html.Div(line))
yield patch
Honestly, the API was the easy part. The transport was the hard part.
Browsers only allow about six open HTTP connections per host, so if every stream gets its own connection, the app freezes after a handful of streams. Our first few fixes treated symptoms, but what finally worked was a single downlink per browser, hosted in a SharedWorker. Every tab and every stream shares it, and closing a tab cancels that tab’s streams.
On the server, it has to work on both WSGI and ASGI:
- Flask/gunicorn serves the downlink in poll mode. Holding a response open per browser pins a gunicorn thread, and the app hangs as soon as you have more browsers than threads.
- FastAPI and Quart keep one open connection per browser and wait on asyncio futures instead of executor threads. The thread-pool version topped out around 500 browsers.
On an 8-core machine, one uvicorn worker held 1,000 streaming browsers at a 3 ms median. On gunicorn, plan for roughly 150 streaming browsers per worker. If you need thousands, use an ASGI backend.
All of this needs processes to share state: which browser owns which stream, which messages it has already seen, who’s still connected. That’s what shared storage is. You get it at dash.ctx.shared_storage: key/value with optional TTL, plus ordered, replayable pub/sub. It ships with three backends:
| Backend | Reach | Use it for |
|---|---|---|
LocalSharedStorage |
one container | single-process apps, development |
DiskcacheSharedStorage |
one machine | multiple workers on one host |
RedisSharedStorage |
many pods | horizontally scaled deployments |
Shared storage is opt-in (shared_storage= or DASH_SHARED_STORAGE). Streaming over HTTP requires it and will fail at startup without it. Streaming over WebSocket doesn’t need it.
React 19 (experimental)
Dash 4.5.0 can run on React 19.2.4. The default is still React 18.3.1. To opt in, set REACT_VERSION=19.2.4 or call dash._dash_renderer._set_react_version("19.2.4"). React 16 support is gone.
React 19 doesn’t publish the UMD builds Dash loads in the browser anymore, so we serve the community umd-react package instead. One gotcha: its version number doesn’t always match the React inside it. umd-react@19.2.0 actually contains React 19.1.0, and one of our earlier attempts shipped that without anyone noticing. We pinned 19.2.4, where the two agree.
The bigger risk is component libraries. A lot of them were built against React 18 or older and use APIs that React 19 removed. We added a small shim that loads right after React and before any component package, so those libraries keep working without a rebuild. If yours breaks on React 19, please open an issue!
Faster renders, and a way to keep them fast
Since 4.2.0, every component a callback returned as children was unmounted and mounted again on every update, even when only its props changed. That reset component state and made large subtrees 3 to 4 times slower. A related bug stopped components like dash-bootstrap-components Tabs from applying their initial state.
4.5.0 updates them in place again. If you actually want a fresh component, wrap it in the new dash.remount.
We fixed two more slowdowns while we were at it:
- Pattern-matching callbacks (
MATCH,ALL,ALLSMALLER) got quadratically slower as the number of matching components grew. A new id-to-path index cuts anALLupdate over 400 components from about 580 ms to about 140 ms, with no changes to your app. - Pages that kept appending children with
Patchgot slower with every append. That’s fixed, and the layout crawl is about 16% faster too.
The regression shipped in 4.2.0 and stuck around through 4.4 because nothing was measuring it. So 4.5.0 adds a benchmark suite that runs on every pull request and fails it if a scenario gets more than twice as slow. CI machines vary, so each run is first scaled against a small reference render, and anything under 5 ms is ignored as timer noise.
Built by the community
Nineteen people outside Plotly wrote 29 of the pull requests in this release, and thirteen of them made their very first contribution to Dash this cycle! Several features in this post are theirs from start to finish: partial pattern matching, request compression, social meta tags and the WebSocket get_prop paths.
@AnnMarieW led the way with seven pull requests, from dcc.Graph cursor positions to bringing Markdown tables back after the dcc.Markdown rewrite. A lot of other folks showed up to fix the bug that was blocking their own app, and that’s how some of the hardest fixes landed: startup races, Windows paths, callbacks registered twice under a reloader.
| Contributor | What they shipped |
|---|---|
| AnnMarieW | Social meta tags, dcc.Graph cursor pixel data, Markdown tables, dcc.Upload multi-file drop, number input steppers, date picker autocomplete |
| i-murray | Partial pattern matching for callback IDs |
| datenzauberai | Callback request compression |
| CNFeffery | dcc.Link scrollToTop, partial WebSocket get_prop reads |
| dajiaohuang | FastAPI reload options, background callback error handler fix, CI status fix |
| BSd3v | FastAPI before_request responses, set_props in background callbacks |
| Aaron-Wrote-This | Patch no longer re-runs callbacks or wipes persistence on existing components |
| mokashang | Startup race under threaded workers |
| raashish1601 | Callbacks registered twice under a reloader |
| jamalkamaladdin | Extra callback registrations when run from a parent directory |
| 0d00ciallo0721 | Callbacks of removed components still firing |
| Mukller | Dict IDs with a dot breaking the page |
| b84330808 | Pages path variables with encoded characters |
| SulimanAbdulrazzaq | Windows extended-length paths in JupyterLab Desktop |
| Sanjays2402 | DASH_PROXY in Jupyter |
| antonymilne | Component generation with spaces in the path |
| cyphercodes | Backend detection in the testing runners |
| brandonschabell | Removed pre-3.9 Python version checks |
| wleeaf | WebSocket callback test before the first HTTP request |
A big thank you to all who contributed! If you’d like to be on this list next time, issues labeled good first issue are a great place to start.
What we ran into
A release this size doesn’t go out in one shot. Here’s how the library evolved over each release.
| Date | Release | What changed |
|---|---|---|
| Oct 29 | 4.5.0 | Shared storage made opt-in; HTTP streaming requires it |
| Oct 23 | rc2 | Security hardening, plotly-cloud back to an optional extra |
| Oct 15 | rc1 | Multi-worker secrets and storage config, startup race fixes |
| Sep 21 | rc0 | Streaming, shared storage, React 19, render fixes |
Scaling past one worker. Streaming worked on a laptop long before it worked on a cluster. On FastAPI with four workers and Redis, 24% to 57% of streams failed under load, because blocking Redis reads starved the worker’s thread pool. Switching to redis.asyncio with one reader per event loop got us to zero errors at 4,000 browsers. Workers also need to agree on a signing secret, so rc1 added DASH_SECRET_KEY and DASH_SHARED_STORAGE. That lets a hosting platform configure every worker without touching the app.
Startup races. Under threaded gunicorn workers, a request could show up while Dash was still registering callbacks and see a half-built app. The result was a burst of 500 errors on the first page load after a restart. A community fix in rc1 puts that setup behind a lock.
Changing our minds. rc0 made plotly-cloud a default dependency. rc2 moved it back to an extra, because it isn’t on conda-forge. Shared storage also started out on by default, and the final release makes it opt-in, so apps that don’t stream don’t pay anything for it.
A test suite we could trust. Our browser tests were pinned to Selenium 4.2, from 2022. It couldn’t drive current Chrome, and every pull request saw flaky failures. Unpinning it exposed more than a dozen tests that clicked and then read the page before the callback had finished. A few callback-count tests had been flaky for about two years: they assumed one callback per keystroke, but the renderer merges keystrokes that arrive together. Every test now waits for the settled state instead of retrying.
Everything else
- Partial pattern matching:
partial_pattern=Truelets a dict ID pattern match components that have extra keys, and it works together withALLandMATCH. - Request compression:
compress_payloadgzips large callback requests from the browser (5,000 bytes and up by default), on Flask, FastAPI and Quart. - Social meta tags:
app.title,app.descriptionandapp.imagenow fill in the preview cards when someone shares a link to your app on social media. - Components:
dcc.Markdownanddcc.Linkhave been rewritten in TypeScript,dcc.LinkgetsscrollToTop, anddcc.Graphclick and hover data now include cursor pixel positions. - Backends: FastAPI and Quart run
before_requesthooks on every route and on the WebSocket handshake, just like Flask, andasync defhook routes work everywhere. - Security: more
hrefprops blockjavascript:URLs, background callbacks withoutcache_byno longer share results between users, and the MCP server can cache per user. - Fixes:
dcc.Uploadwith multiple dropped files,dcc.Inputnumber steppers, Windows long paths in JupyterLab Desktop, and callbacks registered twice underuvicorn --reload.
The full changelog has everything.
Upgrading
pip install -U dash
Most apps will upgrade with no changes. There are three things worth checking:
- If you pin React 16 with
REACT_VERSION, move to 18.3.1 (the default) or give 19.2.4 a try. - If you were relying on callback-returned children being remounted to reset their state, wrap them in
dash.remount. - To stream over HTTP, turn on shared storage with
shared_storage=orDASH_SHARED_STORAGE. Persistent or multi-worker setups needdash[shared_storage],dash[diskcache]ordash[redis], plusDASH_SECRET_KEY.
A huge thank you to all 22 contributors, and to everyone who tested the release candidates and filed issues. I’m excited to see what you build with streaming callbacks, so please share it in a reply below! ![]()