@@ -723,6 +723,11 @@ Special handling for Colab:
723723- `background_callback_manager` - DiskcacheManager or CeleryManager
724724- `on_error` - Global callback error handler
725725
726+ ** WebSocket Callbacks:**
727+ - `websocket_callbacks` - Enable WebSocket for all callbacks (default: `False ` ). Requires FastAPI backend.
728+ - `websocket_allowed_origins` - List of allowed origins for WebSocket connections
729+ - `websocket_inactivity_timeout` - Disconnect WebSocket after inactivity period in ms (default: `300000 ` = 5 minutes). Set to `0 ` to disable.
730+
726731# ## app.run() Parameters
727732
728733- `host` - Server IP (default: `" 127.0.0.1" ` , env: `HOST ` )
@@ -861,6 +866,177 @@ async def async_background(n_clicks):
861866
862867Both DiskcacheManager and CeleryManager support async functions via `asyncio.run()` .
863868
869+ # # WebSocket Callbacks
870+
871+ WebSocket callbacks use a persistent WebSocket connection instead of HTTP POST for callback execution. This reduces latency and connection overhead for applications with frequent callbacks.
872+
873+ # ## Requirements
874+
875+ - ** FastAPI backend required** : WebSocket callbacks only work with FastAPI
876+ - ** SharedWorker support** : Modern browsers (not IE )
877+
878+ # ## Usage
879+
880+ ** Enable globally for all callbacks:**
881+ ```python
882+ from fastapi import FastAPI
883+ from dash import Dash
884+
885+ server = FastAPI()
886+ app = Dash(__name__ , server = server, websocket_callbacks = True )
887+ ```
888+
889+ ** Enable per- callback:**
890+ ```python
891+ @ app.callback(
892+ Output(' output' , ' children' ),
893+ Input(' input' , ' value' ),
894+ websocket = True # Use WebSocket for this callback only
895+ )
896+ def update(value):
897+ return f " Value: { value} "
898+ ```
899+
900+ # ## Configuration
901+
902+ ```python
903+ app = Dash(
904+ __name__ ,
905+ server = server,
906+ websocket_callbacks = True ,
907+ websocket_inactivity_timeout = 300000 , # 5 minutes (default)
908+ websocket_allowed_origins = [' https://example.com' ],
909+ )
910+ ```
911+
912+ - ** `websocket_callbacks` ** - Enable WebSocket for all callbacks (default: `False ` )
913+ - ** `websocket_inactivity_timeout` ** - Close WebSocket after period of inactivity in milliseconds (default: `300000 ` = 5 minutes). Heartbeats do not count as activity. Set to `0 ` to disable timeout. Connection automatically reconnects when needed.
914+ - ** `websocket_allowed_origins` ** - List of allowed origins for WebSocket connections (security)
915+
916+ # ## Architecture
917+
918+ ```
919+ ┌─────────────────────────────────────────────────────────────────────────┐
920+ │ Browser Tab 1 Browser Tab 2 │
921+ │ ┌─────────────┐ ┌─────────────┐ │
922+ │ │ Renderer │ │ Renderer │ │
923+ │ └──────┬──────┘ └──────┬──────┘ │
924+ │ │ postMessage │ postMessage │
925+ │ └────────────┬───────────────────────┘ │
926+ │ ▼ │
927+ │ ┌─────────────────────┐ │
928+ │ │ SharedWorker │ (one per origin) │
929+ │ │ dash- ws- worker │ │
930+ │ └──────────┬──────────┘ │
931+ └────────────────────│────────────────────────────────────────────────────┘
932+ │ WebSocket
933+ ▼
934+ ┌─────────────────────────────────────────────────────────────────────────┐
935+ │ Server (FastAPI) │
936+ │ WebSocket Endpoint: / _dash- ws- callback │
937+ └─────────────────────────────────────────────────────────────────────────┘
938+ ```
939+
940+ ** Connection & Reconnection Flow:**
941+ ```
942+ Renderer SharedWorker Server
943+ │ │ │
944+ │──[CONNECT ]──────────────────> │ │
945+ │ │──[WebSocket Connect]──> │
946+ │< ─[CONNECTED ]─────────────────│< ─[Connected]───────────│
947+ │ │ │
948+ │──[CALLBACK_REQUEST ]─────────> │──[callback request]───> │
949+ │< ─[CALLBACK_RESPONSE ]─────────│< ─[callback response]───│
950+ │ │ │
951+ │ (inactivity) │ (heartbeat check) │
952+ │ │──[close 4001 ]─────────> │
953+ │< ─[DISCONNECTED ]──────────────│ │
954+ │ │ │
955+ │──[CALLBACK_REQUEST ]─────────> │──[reconnect + send]───> │
956+ │< ─[CALLBACK_RESPONSE ]─────────│< ─[response]────────────│
957+ ```
958+
959+ - ** SharedWorker** : Single WebSocket connection shared across browser tabs
960+ - ** Heartbeat** : Periodic ping/ pong to detect dead connections (30s interval)
961+ - ** Inactivity timeout** : Closes connection after no actual callback activity (not heartbeats)
962+ - ** Auto- reconnect** : Reconnects automatically when a callback is triggered after timeout
963+
964+ # ## Long-Running Callbacks with set_props/get_props
965+
966+ WebSocket callbacks can stream updates to the client during execution using `set_props()` and read current component values using `ctx.get_websocket()` :
967+
968+ ```python
969+ import asyncio
970+ from dash import callback, Output, Input, set_props, ctx
971+
972+ @ callback(
973+ Output(' result' , ' children' ),
974+ Input(' start-btn' , ' n_clicks' ),
975+ prevent_initial_call = True
976+ )
977+ async def long_running_task(n_clicks):
978+ ws = ctx.get_websocket()
979+ if not ws:
980+ return " WebSocket not available"
981+
982+ # Stream progress updates to the client
983+ for i in range (100 ):
984+ await asyncio.sleep(0.1 )
985+ set_props(' progress-bar' , {' value' : i + 1 })
986+ set_props(' status' , {' children' : f ' Processing step { i + 1 } /100... ' })
987+
988+ # Read current value from another component
989+ current_value = await ws.get_prop(' input-field' , ' value' )
990+
991+ return f " Completed! Input was: { current_value} "
992+ ```
993+
994+ ** API :**
995+ - `set_props(component_id, props_dict)` - Stream prop updates immediately to client
996+ - `ctx.get_websocket()` - Get WebSocket interface (returns `None ` if not in WS context)
997+ - `await ws.get_prop(component_id, prop_name)` - Read current prop value from client
998+ - `await ws.set_prop(component_id, prop_name, value)` - Set single prop (async version)
999+ - `await ws.close(code, reason)` - Close the WebSocket connection
1000+
1001+ # ## Connection Hooks
1002+
1003+ Use hooks to validate connections and messages:
1004+
1005+ ```python
1006+ from dash import Dash, hooks
1007+
1008+ @ hooks.websocket_connect()
1009+ async def validate_connection(websocket):
1010+ """ Validate WebSocket connection before accepting."""
1011+ session_id = websocket.cookies.get(" session_id" )
1012+ if not session_id:
1013+ return (4001 , " No session cookie" )
1014+ if not await is_valid_session(session_id):
1015+ return (4002 , " Invalid session" )
1016+ return True # Allow connection
1017+
1018+ @ hooks.websocket_message()
1019+ async def validate_message(websocket, message):
1020+ """ Validate each WebSocket message."""
1021+ session_id = websocket.cookies.get(" session_id" )
1022+ if not await is_session_active(session_id):
1023+ return (4002 , " Session expired" )
1024+ return True # Allow message
1025+ ```
1026+
1027+ ** Hook Return Values:**
1028+ - `True ` (or truthy) - Allow connection/ message
1029+ - `False ` - Reject with default code (4001 )
1030+ - `(code, reason)` - Reject with custom close code and reason
1031+
1032+ # ## Key Files
1033+
1034+ - `dash/ dash.py` - WebSocket config in `_generate_config()`
1035+ - `dash/ dash- renderer/ src/ utils/ workerClient.ts` - Browser- side SharedWorker client
1036+ - `@ plotly/ dash- websocket- worker/ src/ WebSocketManager.ts` - WebSocket connection management
1037+ - `@ plotly/ dash- websocket- worker/ src/ worker.ts` - SharedWorker entry point
1038+ - `dash/ backends/ _fastapi.py` - Server- side WebSocket handler
1039+
8641040# # Security
8651041
8661042# ## XSS Protection
0 commit comments