Introducing Dash MCP + Building Dash Apps with Coding Agents. Reserve your spot.

blog icon darkBlog_icon_purple

Blog

doc_icon_darkDoc icon purple

Docs

login_icon_darkLogin icon purple

Log In

author photo

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.websocket
while 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 table

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)
Bluesky icon
X icon
Instagram icon
Youtube icon
Medium icon
Facebook icon

Product

© 2026
Plotly. All rights reserved.
Cookie Preferences