
Nathan Drezner
May 29, 2026
Dash 4.2: WebSocket Callbacks
Back in 2020, we posted a prototype showing WebSocket-powered callbacks in Dash. The thread filled up quickly: "This totally brings Dash to a different level." "This is desperately needed for most modern applications."
Today, we’re excited to be able to add websocket support to Dash: Dash 4.2 adds WebSocket callbacks to core.
What callbacks couldn't do before
Dash callbacks are request-response. The browser sends inputs, the server runs your function, the result comes back when the function finishes. There was no way to send anything to the browser while the callback executed.
It’s possible to stream to the frontend with websocket callbacks – for example, running the Conway’s Game of Life simulation.
This created real friction for a few common patterns. Progress bars required setting up Celery or Diskcache through the background callback API just to send incremental updates. Live data meant polling with dcc.Interval, which breaks silently if your callback takes longer than the polling interval. People built creative workarounds (throttled intervals, caching layers, recursive callback tricks), but they were all working around the same missing piece: the server couldn't push.
How WebSocket callbacks work
A WebSocket callback holds an open connection between the server and browser for the duration of the callback. Two new primitives make this useful:
set_props sends a component update to the browser immediately, without waiting for the callback to return. Call it as many times as you want during execution.
get_prop reads the current value of a component property from the browser, mid-callback. This lets a long-running callback adapt to what the user is doing without declaring everything as State up front.
Here's a callback that streams progress:
@callback(Output("status", "children"), Input("btn", "n_clicks"),prevent_initial_call=True)async def process_data(n):set_props("progress", {"value": 0})for i in range(100):await do_work()set_props("progress", {"value": i + 1})return "Complete"
That's it. No task queue, no message broker, no polling loop.
Persistent callbacks
Some things don't fit the input-output model. A live dashboard streaming market data doesn't start from a button click, and it doesn't return a single value.
Persistent callbacks handle this. They start when a client connects, run for the session, and stop when the tab closes:
@callback(persistent=True)async def stream_prices():ws = ctx.websocketwhile not ws.is_shutdown:set_props("price-table", {"rowData": get_latest_prices()})await asyncio.sleep(0.5)
No Input, no Output, no loading indicator in the browser tab. The callback just runs in the background and pushes updates with set_props.
Reading browser state mid-execution
A streaming callback can check which item a user selected in a dropdown and change what it's streaming, without restarting:
symbol = await ws.get_prop("symbol-select", "value")
This makes callbacks feel less like one-shot functions and more like server-side sessions that respond to what's happening in the UI.

Real-time updates to tables and grids are possible with websocket callbacks.
Use cases we're seeing
We put together a collection of demo apps to stress-test persistent callbacks, and the range ended up being a good illustration of what's possible:
- An IoT sensor dashboard with 4 simulated sensors updating at ~10Hz on dual-axis charts
- A nuclear reactor control room simulator with a core temperature heatmap and components updating at different rates (20Hz for the heatmap, 2Hz for metrics, 0.5Hz for logs)
- A drum machine with a 16-step sequencer and real-time playhead animation
- A particle physics sandbox with click-to-spawn particles and adjustable gravity
- Conway's Game of Life on a 50x50 grid at adjustable frame rates
These demos run entirely on persistent callbacks and set_props. The nuclear sim is a good example of something that would have been impractical with polling: three independent update loops running at different frequencies from a single callback.
Getting started
WebSocket callbacks require a FastAPI or Quart backend. If you're on Flask, switching is a one-line change.
Enable globally or per-callback:
app = Dash(backend="fastapi", websocket_callbacks=True)# or per-callback@callback(..., websocket=True)