-
dash/dash.py- MainDashapplication class (~2000 lines). Orchestrates the server backend, layout management, callback registration, routing, and asset serving. Key methods:layoutproperty,callback(),clientside_callback(),run(). -
dash/backends/- Server backend implementations. See Server Backends section for details. -
dash/_callback.py- Callback registration and execution. Containscallback()decorator (usable as@dash.callbackwithout app instance),clientside_callback(), andregister_callback()which inserts callbacks into the callback map. -
dash/dependencies.py- Dependency classes for callbacks:Input- Triggers callback when value changesOutput- Component property to update (supportsallow_duplicate=True)State- Read value without triggering callbackClientsideFunction- Reference to JS function for clientside callbacks- Wildcards:
MATCH,ALL,ALLSMALLERfor pattern-matching IDs
-
dash/development/base_component.py-Componentbase class withComponentMetametaclass. All Dash components inherit from this. Components auto-register inComponentRegistryand serialize to JSON viato_plotly_json(). -
dash/_pages.py- Multi-page app support.PAGE_REGISTRYholds registered pages,register_page()decorator registers page modules with routes.
The layout defines the UI as a tree of components:
app.layout = html.Div([
dcc.Input(id='input', value='initial'),
html.Div(id='output')
])- Static layout: Assigned directly as a component tree
- Dynamic layout: Assigned as a function that returns components (called on each page load, useful for per-session state)
- Layout is serialized to JSON and sent to the React frontend via
/_dash-layout - Components can contain other components via
childrenprop - Component IDs can be strings or dicts (for pattern-matching callbacks)
@app.callback or @dash.callback:
@app.callback(Output('output', 'children'), Input('input', 'value'))
def update(value):
return f'You entered: {value}'Server-side Python function called when inputs change. Outputs update component properties.
app.clientside_callback:
app.clientside_callback(
"""function(value) { return 'You entered: ' + value; }""",
Output('output', 'children'),
Input('input', 'value')
)JavaScript function runs in browser. Faster for simple transformations, no server round-trip. Can reference window.dash_clientside.namespace.function_name or inline JS string.
background=True:
@app.callback(Output('output', 'children'), Input('btn', 'n_clicks'),
background=True, manager=diskcache_manager,
running=[(Output('btn', 'disabled'), True, False)],
progress=[Output('progress', 'value')])
def compute(set_progress, n_clicks):
for i in range(10):
set_progress(i * 10)
time.sleep(1)
return 'Done'Callbacks executed in separate process via Celery or Diskcache manager. Supports progress updates, running state changes, and cancel inputs. See Background Callbacks section for details.
@app.callback(
Output({'type': 'output', 'index': MATCH}, 'children'),
Input({'type': 'input', 'index': MATCH}, 'value')
)
def update(value):
return valueUse dict IDs with wildcards (MATCH, ALL, ALLSMALLER) to target dynamically-generated components.
/_dash-layout- Returns initial component tree as JSON/_dash-dependencies- Returns callback definitions/_dash-update-component- Executes callbacks, returns updated props/_dash-component-suites/<package>/<path>- Serves component JS/CSS assets/assets/<path>- Serves static assets from app's assets folder
Dash supports multiple web server backends. The backend abstraction is in dash/backends/.
| Backend | Type | Install | Use Case |
|---|---|---|---|
| Flask (default) | WSGI (sync) | pip install dash |
Standard deployments, simplicity |
| Quart | ASGI (async) | pip install dash[quart] |
Async callbacks, WebSocket support |
| FastAPI | ASGI (async) | pip install dash[fastapi] |
OpenAPI docs, async, modern Python |
Default (Flask):
from dash import Dash
app = Dash(__name__)With existing server instance:
from flask import Flask
from dash import Dash
server = Flask(__name__)
app = Dash(__name__, server=server)Quart backend:
from quart import Quart
from dash import Dash
server = Quart(__name__)
app = Dash(__name__, server=server)FastAPI backend:
from fastapi import FastAPI
from dash import Dash
server = FastAPI()
app = Dash(__name__, server=server)
# Run with: uvicorn module:app.server --reloadThe backend system uses an abstract interface:
-
BaseDashServer(dash/backends/base_server.py) - Abstract base class defining the server interface. All backends implement this. -
RequestAdapter- Normalizes HTTP request objects across frameworks. Provides unified access toargs,cookies,headers,get_json(), etc. -
ResponseAdapter- Normalizes response creation. Handlesset_cookie(),set_header(),set_response(). -
get_backend(name)- Factory function to get backend class by name ("flask","quart","fastapi"). -
get_server_type(server)- Auto-detects backend from a server instance.
Flask (dash/backends/_flask.py):
FlaskDashServer- Wraps Flask appFlaskRequestAdapter- Usesflask.requestproxyFlaskResponseAdapter- Usesflask.Response- Compression via
flask-compress
Quart (dash/backends/_quart.py):
QuartDashServer- Wraps Quart app (async Flask API)QuartRequestAdapter- Usesquart.requestproxyQuartResponseAdapter- Usesquart.Response- All route handlers are
async def - Compression via
quart-compress
FastAPI (dash/backends/_fastapi.py):
FastAPIDashServer- Wraps FastAPI appFastAPIRequestAdapter- Uses context variable for current requestFastAPIResponseAdapter- Uses Starlette responsesDashMiddleware- Consolidated ASGI middleware for request handling- Runs with uvicorn, supports hot reload
- Built-in GZip compression
All backends implement:
class BaseDashServer(ABC):
def create_app(name, config) -> server # Create new server
def add_url_rule(rule, view_func, ...) # Register routes
def before_request(func) # Request hooks
def after_request(func) # Response hooks
def run(dash_app, host, port, debug) # Start dev server
def make_response(data, mimetype, status) # Create response
def jsonify(obj) # JSON response
def setup_index(dash_app) # Register / route
def serve_callback(dash_app) # Callback endpoint
def setup_component_suites(dash_app) # JS/CSS servingapp = Dash(__name__)
# Get the underlying server
app.server # Flask/Quart/FastAPI instance
# Get the backend wrapper
app.backend # BaseDashServer subclass instance
app.backend.server_type # "flask", "quart", or "fastapi"
# Access request in callbacks
from dash import dash
dash.get_app().backend.request_adapter() # RequestAdapter instancedash/dash-renderer/src/ contains the TypeScript/React frontend. See RENDERER.md for detailed documentation on:
- Layout traversal (
crawlLayout) andchildren_props - Component resolution from
window[namespace][type] - Callback triggering via
setPropsandnotifyObservers - Redux store structure (layout, paths, callbacks, graphs)
- Observer system for callback processing
window.dash_clientsideAPIwindow.dash_component_apiAPI
Dash supports multiple React versions. Configured in dash/_dash_renderer.py.
Available versions: 18.3.1 (default), 18.2.0, 19.2.4 (experimental)
React 19 has no official UMD builds; Dash serves the umd-react package for it, plus a small shim (dash-renderer/build/react-shim.min.js, source dash/dash-renderer/src/react-shim.js) loaded right after react-dom and before any component package. The shim stubs the React <=18 secret internals (ReactCurrentOwner) some component libraries touch at load time, redirects the legacy react.element $$typeof symbol so libraries that pre-bundled a React <=18 jsx-runtime don't hit React error #525, and provides window.ReactJSXRuntime, the global that component bundles externalize react/jsx-runtime to.
Convention for component libraries: externalize react/jsx-runtime and react/jsx-dev-runtime using the defensive external expression found in components/dash-core-components/webpack.config.js (jsxRuntimeExternal), not a bare 'ReactJSXRuntime' string. The expression falls back to building the runtime from window.React.createElement when the global is missing, so the same bundle works on Dash versions that predate the shim. A bare 'ReactJSXRuntime' external throws ReactJSXRuntime is not defined / Cannot read properties of undefined (reading 'jsx') at bundle load on older Dash.
Set via environment variable (experimental):
REACT_VERSION=19.2.4 python app.pyOr programmatically before creating the app:
from dash._dash_renderer import _set_react_version
_set_react_version("19.2.4")
from dash import Dash
app = Dash(__name__)This is useful for compatibility with older component libraries that require React 16.
Multi-page apps use dash/_pages.py with automatic routing via dcc.Location.
Each page module calls register_page():
# pages/analytics.py
from dash import register_page, html
register_page(__name__) # infers path /analytics from module name
layout = html.Div("Analytics page")PAGE_REGISTRY-OrderedDictstoring all registered pages with metadataregister_page(module, path=None, ...)- Registers page with inferred or explicit path, title, description, image
When use_pages=True, Dash injects page_container as the layout (dash/dash.py:148-158):
page_container = html.Div([
dcc.Location(id="_pages_location", refresh="callback-nav"),
html.Div(id="_pages_content"), # current page layout injected here
dcc.Store(id="_pages_store"), # stores page title/metadata
])dcc.Locationtracks browser URL changes- Internal callback listens to
pathnameandsearchinputs _path_to_page()matches URL to registered page inPAGE_REGISTRY- Page layout injected into
_pages_contentdiv
Pages can capture URL variables:
register_page(__name__, path_template="/asset/<asset_id>")
def layout(asset_id=None):
return html.Div(f"Asset: {asset_id}")_parse_path_variables() extracts variables via regex and passes them as kwargs to the layout function.
_import_layouts_from_pages() walks the pages/ folder:
- Skips files starting with
_or. - Only imports
.pyfiles containingregister_page - Auto-assigns
layoutattribute from each module to the registry
Pages sorted by: numeric order → string order → no order → module name. Home page (/) defaults to order 0.
The assets/ folder is automatically scanned at startup (dash/dash.py:_walk_assets_directory):
.cssfiles → appended to stylesheets.jsfiles → appended to scriptsfavicon.ico→ used as app favicon- Files matching
assets_ignoreregex are skipped
Resources load in this order (dash/dash.py:1127-1165):
- React dependencies (from dash-renderer)
- Component library scripts (dash-html-components, dash-core-components, etc.)
- External scripts (
external_scriptsparameter) - Dash renderer bundle
- Clientside callback scripts (inline)
CSS follows similar ordering with external stylesheets first.
Component assets use fingerprinted URLs for cache busting (dash/fingerprint.py):
/_dash-component-suites/dash_core_components/dash_core_components.v2_14_0m1699900000.min.js
- Fingerprinted resources: 1-year cache header
- Non-fingerprinted: ETag validation
- Asset files: query string
?m={modification_time}
Dash(
assets_folder='assets', # path to assets directory
assets_url_path='assets', # URL path segment
assets_ignore='.*ignored.*', # regex to skip files
assets_external_path=None, # CDN base URL for assets
serve_locally=True, # True=local files, False=CDN
external_scripts=[], # additional JS URLs
external_stylesheets=[], # additional CSS URLs
)app.get_asset_url(path) returns the correct URL accounting for requests_pathname_prefix (important for Dash Enterprise deployments where apps have URL prefixes).
Debug mode enables developer tools (dash/dash.py:_setup_dev_tools):
app.run(debug=True)
# Or via environment: DASH_DEBUG=trueapp.enable_dev_tools(
dev_tools_ui=True, # show error UI overlay
dev_tools_props_check=True, # validate component prop types
dev_tools_serve_dev_bundles=True, # use development JS (better errors)
dev_tools_hot_reload=True, # auto-reload on file changes
dev_tools_prune_errors=True, # strip internal frames from tracebacks
)Environment variables: DASH_DEBUG, DASH_UI, DASH_PROPS_CHECK, DASH_HOT_RELOAD, etc.
PreventUpdate - Skip updating outputs without error:
from dash.exceptions import PreventUpdate
@app.callback(Output('out', 'children'), Input('in', 'value'))
def update(value):
if not value:
raise PreventUpdate
return valueno_update - Skip specific outputs in multi-output callbacks:
from dash import no_update
@app.callback(Output('a', 'children'), Output('b', 'children'), Input('in', 'value'))
def update(value):
return value, no_update # only updates 'a'Callbacks support on_error for custom error handling:
def handle_error(err):
logging.error(f"Callback failed: {err}")
return "Error occurred" # returned to output
@app.callback(Output('out', 'children'), Input('in', 'value'), on_error=handle_error)
def update(value):
return 1 / 0 # triggers error handlerApp-level error handler set via constructor.
- Layout validation: When
suppress_callback_exceptions=False(default), checks that callback IDs exist in layout - Callback validation:
dev_tools_validate_callbacks=Truechecks for circular dependencies - Props checking: Validates component prop types against schema in dev mode
When enabled, a watch thread monitors:
assets/folder for CSS/JS changes- Component package directories
Frontend polls /_reload-hash and triggers reload when hash changes. Configurable via hot_reload_interval (default 3s) and hot_reload_watch_interval (default 0.5s).
Background callbacks execute in separate processes, allowing the main server to remain responsive. Managed by dash/background_callback/managers/.
from dash import callback, Input, Output
from dash.background_callback import DiskcacheManager
cache_manager = DiskcacheManager()
@callback(
Output("result", "children"),
Input("button", "n_clicks"),
background=True,
manager=cache_manager,
interval=500, # polling interval in ms
)
def compute(n_clicks):
# Expensive computation
return resultDiskcacheManager (dash/background_callback/managers/diskcache_manager.py):
- Uses
diskcache.Cachefor persistent storage - Spawns
multiprocess.Processfor each job - Results stored on disk, survives server restarts
- Good for single-server deployments
CeleryManager (dash/background_callback/managers/celery_manager.py):
- Requires Celery app with result backend (Redis/RabbitMQ)
- Jobs distributed across Celery workers
- Supports horizontal scaling
- Good for production multi-worker deployments
from celery import Celery
from dash.background_callback import CeleryManager
celery_app = Celery(__name__, broker="redis://localhost:6379/0")
cache_manager = CeleryManager(celery_app)The progress parameter defines outputs updated during execution:
@callback(
Output("result", "children"),
Input("button", "n_clicks"),
progress=Output("progress-bar", "value"),
progress_default=0,
background=True,
manager=cache_manager,
)
def compute(set_progress, n_clicks):
for i in range(100):
set_progress(i)
time.sleep(0.1)
return "Complete"set_progressis injected as first argument whenprogressis specified- Can be single Output or list of Outputs
progress_defaultsets value when callback not running
The running parameter updates outputs while the job executes:
@callback(
Output("result", "children"),
Input("button", "n_clicks"),
running=[
(Output("button", "disabled"), True, False),
(Output("status", "children"), "Computing...", "Ready"),
],
background=True,
manager=cache_manager,
)
def compute(n_clicks):
time.sleep(5)
return "Done"Each tuple: (Output, value_while_running, value_when_complete)
The cancel parameter specifies inputs that abort the job:
@callback(
Output("result", "children"),
Input("start-btn", "n_clicks"),
cancel=[Input("cancel-btn", "n_clicks")],
background=True,
manager=cache_manager,
)
def compute(n_clicks):
# Job terminates if cancel-btn clicked
return resultManagers call terminate_job() which kills the process (Diskcache) or revokes the task (Celery).
Results can be cached to avoid recomputation:
def get_user_id():
return flask.session.get("user_id")
cache_manager = DiskcacheManager(
cache_by=[get_user_id], # cache key includes user ID
expire=3600, # TTL in seconds
)cache_by- List of functions whose return values are included in cache keyexpire- Time-to-live for cached resultscache_args_to_ignore- Argument indices to exclude from cache key
- Initial request: Frontend triggers callback, backend returns
cacheKeyandjobID - Polling: Frontend polls
/_dash-update-component?cacheKey=...&job=...at configured interval - Progress: Each poll returns current progress value if set
- Completion: When job finishes, poll returns final result
- Cleanup: Results cleared from cache (unless
cache_byspecified)
Cache key is SHA256 hash of: function source + arguments + triggered inputs + cache_by values.
dash/_callback.py:188-219- Background spec constructiondash/background_callback/managers/__init__.py-BaseBackgroundCallbackManagerabstract classdash/background_callback/managers/diskcache_manager.py- Diskcache implementationdash/background_callback/managers/celery_manager.py- Celery implementationdash/dash-renderer/src/actions/callbacks.ts:458-685- Frontend polling logic
Dash apps can run directly in Jupyter notebooks and JupyterLab. The integration is handled by dash/_jupyter.py.
app.run(
jupyter_mode="inline", # Display in notebook cell (default)
jupyter_width="100%", # IFrame width
jupyter_height=650, # IFrame height in pixels
)| Mode | Behavior |
|---|---|
"inline" |
App displays in notebook cell via IFrame |
"external" |
Prints URL, user opens in browser tab |
"jupyterlab" |
Opens in dedicated JupyterLab tab |
"tab" |
Auto-opens URL in new browser tab |
app.run()detects Jupyter environment viaget_ipython()- Server starts in background daemon thread
- Jupyter comm protocol negotiates proxy configuration
- App displays according to selected mode
app.run() in notebook
↓
Detect Jupyter → Start server in background thread
↓
Comm request → Extension responds with base_url
↓
Compute dashboard URL with proxy path
↓
Display: IFrame (inline) / URL (external) / Tab (jupyterlab)
Classic Jupyter notebooks use dash/nbextension/:
main.js- Registers "dash" comm targetdash.json- Extension loader configuration
The extension handles comm messages:
base_url_request→ responds with server URL and base path- Enables proper proxy routing in JupyterHub environments
JupyterLab uses @plotly/dash-jupyterlab/:
src/index.ts- TypeScript plugin implementingJupyterFrontEndPluginDashIFrameWidget- Lumino widget for rendering apps in tabs
Handles messages:
base_url_request→ responds with JupyterLab server configshow→ creates dedicated tab with IFrame widget
Compatible with JupyterLab 2.x, 3.x, and 4.x.
In JupyterHub/proxy environments, the extension negotiates requests_pathname_prefix:
# Computed from Jupyter base path
requests_pathname_prefix = "/user/username/proxy/8050/"This ensures callbacks route correctly through the Jupyter proxy.
Special handling for Colab:
- Uses
google.colab.output.serve_kernel_port_as_iframe()for inline - Uses
google.colab.output.serve_kernel_port_as_window()for external - Only supports "inline" and "external" modes
dash/_jupyter.py-JupyterDashclass, comm handling, server threaddash/nbextension/main.js- Classic notebook extension@plotly/dash-jupyterlab/src/index.ts- JupyterLab extension
Basic Setup:
name- Application name (default: infers from__name__)server- Server instance (Flask, Quart, or FastAPI) orTrueto create Flask (default:True)title- Browser tab title (default:"Dash")update_title- Title during callbacks (default:"Updating...")
Assets & Resources:
assets_folder- Path to assets directory (default:"assets")assets_url_path- URL path for assets (default:"assets")assets_ignore- Regex to exclude assets (default:"")serve_locally- Serve from local vs CDN (default:True)external_scripts- Additional JS URLsexternal_stylesheets- Additional CSS URLs
Routing:
url_base_pathname- Base URL prefix for entire apprequests_pathname_prefix- Prefix for AJAX requestsroutes_pathname_prefix- Prefix for API routes
Multi-Page:
use_pages- Enable pages system (default: auto-detect)pages_folder- Path to pages directory (default:"pages")
Behavior:
suppress_callback_exceptions- Skip callback validation (default:False)prevent_initial_callbacks- Skip callbacks on load (default:False)background_callback_manager- DiskcacheManager or CeleryManageron_error- Global callback error handlershared_storage- Cross-process state + pub/sub backend (default:LocalSharedStorage). PassNoneto disable, or aBaseSharedStoragesubclass/instance to swap. See Shared Storage.
WebSocket Callbacks:
websocket_callbacks- Enable WebSocket for all callbacks (default:False). Requires FastAPI backend.websocket_allowed_origins- List of allowed origins for WebSocket connectionswebsocket_inactivity_timeout- Disconnect WebSocket after inactivity period in ms (default:300000= 5 minutes). Set to0to disable.
host- Server IP (default:"127.0.0.1", env:HOST)port- Server port (default:8050, env:PORT)debug- Enable dev tools (default:False, env:DASH_DEBUG)jupyter_mode- Display mode:"inline","external","tab"
| Variable | Purpose |
|---|---|
DASH_DEBUG |
Enable debug mode |
DASH_URL_BASE_PATHNAME |
Base URL prefix |
DASH_SUPPRESS_CALLBACK_EXCEPTIONS |
Skip validation |
DASH_HOT_RELOAD |
Enable hot reload |
DASH_PROPS_CHECK |
Validate prop types |
DASH_PRUNE_ERRORS |
Simplify tracebacks |
HOST |
Server host |
PORT |
Server port |
Store data client-side with configurable persistence:
dcc.Store(id='my-store', storage_type='local', data={'key': 'value'})| Storage Type | Persists | Scope | Use Case |
|---|---|---|---|
'memory' |
Page view only | Tab | Temporary state, debugging |
'session' |
Browser session | Tab | Form state, filters |
'local' |
Forever | All tabs | User preferences, settings |
Usage pattern:
@app.callback(Output('output', 'children'), Input('store', 'data'))
def use_store(data):
return data['key']
@app.callback(Output('store', 'data'), Input('input', 'value'))
def update_store(value):
return {'key': value}Automatically persist user edits to component props:
dcc.Dropdown(
id='dropdown',
options=[...],
persistence=True, # Enable persistence
persistence_type='local', # local, session, or memory
persisted_props=['value'], # Props to persist (default varies by component)
)persistence-Trueor unique key to enablepersistence_type- Storage backend (default:'local')persisted_props- List of prop names to persist
Supported components: Input, Dropdown, Checklist, RadioItems, Slider, RangeSlider, DatePickerSingle, DatePickerRange, Textarea, Tabs, DataTable.
| Need | Solution |
|---|---|
| Server-controlled state | dcc.Store with callbacks |
| Remember user selections | Component persistence=True |
| Share state across tabs | dcc.Store with storage_type='local' |
| Session-only state | persistence_type='session' |
Backend-agnostic server-side state shared across worker processes: a
cross-process key/value store plus an ordered, replayable publish/subscribe
channel. Unlike dcc.Store (which lives in the browser), shared storage lives
on the server and lets callbacks running in different workers see the same keys
and topics without standing up an external service like Redis.
Enabled by default on every app. It starts lazily on first use, so it costs nothing until touched.
import dash
from dash import Input, Output, callback
@callback(Output("out", "children"), Input("btn", "n_clicks"))
def handler(n):
store = dash.ctx.shared_storage # inside a callback
store.set("clicks", n)
return store.get("clicks", 0)
# Or off the app directly:
app = dash.Dash()
app.shared_storage.set("key", {"any": "json-compatible value"})Values (and published messages) must be JSON-compatible — dict / list / str
/ int / float / bool / None — the same constraint as dcc.Store and callback
outputs. Messages are encoded with msgspec (msgpack), a hard dependency.
app.shared_storage (and dash.ctx.shared_storage) is a BaseSharedStorage:
| Method | Purpose |
|---|---|
get(key, default=None) |
Read a value |
set(key, value, ttl=None) |
Write a value; ttl = optional lifetime in seconds |
delete(key) |
Remove a key |
publish(topic, message) |
Append a message to a topic |
subscribe(topic, replay_from=None) |
Return a Subscription |
Key expiry (TTL). set(key, value, ttl=<seconds>) gives a key a bounded
lifetime; once it elapses, reads return the default again. ttl=None (the
default) never expires. Expiry is lazy/best-effort — an expired key is dropped
on its next read, not at a guaranteed instant. Each backend uses its native
mechanism: a monotonic deadline on the in-memory owner, diskcache expire,
Redis PX. Use it for session-scoped or cache-like state you don't want to
accumulate unboundedly.
Ordered pub/sub. Each publish to a topic gets a monotonically increasing
sequence number (per topic, starting at 1; 0 means "before the first
message"). A subscriber receives every message published after it subscribed,
in order:
# Producer (one worker):
store.publish("progress", {"pct": 50})
# Consumer (another worker/callback):
with store.subscribe("progress") as sub:
for message in sub: # or: async for message in sub
print(message["pct"])Replay on reconnect. Pass replay_from=<last-seen-seq> to resume after a
drop; buffered messages since that cursor replay first, so a reconnecting
consumer doesn't miss messages. If the consumer fell farther behind than the
bounded buffer holds, the subscription raises SharedStorageGap (an explicit
gap rather than a silent hole). A Subscription is iterable synchronously
(for) or asynchronously (async for), is a context manager, and has
close().
Three backends ship, differing only in how far state reaches. Pick by the deployment topology — critically, whether the app runs behind a load balancer as more than one container/pod (see Deployment Topology below).
| Backend | Backing store | Reaches | Extra |
|---|---|---|---|
LocalSharedStorage (default) |
in-memory, owner-elected socket | one container (all its workers) | none |
DiskcacheSharedStorage |
a diskcache.Cache |
one host (all processes sharing the dir) | dash[diskcache] |
RedisSharedStorage |
Redis (Streams for pub/sub) | any process/container/pod | dash[redis] |
All three implement the same BaseSharedStorage contract (KV + ordered,
replayable pub/sub with SharedStorageGap on buffer overrun), so app code is
identical across them — only the constructor differs.
from dash import Dash
from dash._shared_storage import (
LocalSharedStorage, DiskcacheSharedStorage, RedisSharedStorage,
)
Dash(shared_storage=LocalSharedStorage) # default
Dash(shared_storage=DiskcacheSharedStorage(directory="/tmp/ss"))
Dash(shared_storage=RedisSharedStorage(url="redis://host:6379"))
Dash(shared_storage=None) # disabledWhen disabled, accessing app.shared_storage / dash.ctx.shared_storage
raises SharedStorageError.
LocalSharedStorage — in-memory, no external service. On first use every
worker races to become the single owner for the machine by binding a stable
address (AF_UNIX on POSIX, 127.0.0.1 loopback on Windows); the winner hosts
the engine, losers proxy over the socket. The bind is the lease — when the
owner dies the address frees and a survivor re-elects. A single-process
deployment is its own owner and pays no socket overhead. Knobs: namespace
(defaults to a hash of cwd + argv[0]), buffer_size (default 32 — small on
purpose, since each topic retains that many arbitrary payloads; raise it for a
wider reconnect window).
Durability is controlled by mode (the key/value store only — pub/sub is
always transient):
mode |
Writes | On owner death / restart |
|---|---|---|
"memory" (default) |
none | re-elects cold (state lost) |
"persist" |
write-through on every set/delete | re-elected/fresh owner recovers from disk |
"persist-reset" |
flushed every flush_intervals (default 60) + on clean exit |
recovers to the last snapshot (up to one interval may be lost on an unclean crash) |
Only the owner persists. The on-disk store is chunked (keys sharded across
msgpack chunk files via a key→chunk index, so a mutation rewrites only the
affected chunk — adapted from raposa's BinaryStorage); each chunk write is
atomic (temp file + os.replace). TTLs survive restarts (stored as wall-clock
deadlines; expired keys are dropped on load). path sets the store directory
(default: a per-namespace folder under the user cache dir). Recovery runs on
every election, so persisted data also survives owner re-election:
Dash(shared_storage=LocalSharedStorage(mode="persist"))
Dash(shared_storage=LocalSharedStorage(mode="persist-reset", flush_interval=30))DiskcacheSharedStorage — KV + an append-log pub/sub on a diskcache.Cache,
the same store DiskcacheManager uses; pass an existing cache to share one.
Every process on one host that opens the same directory shares state, so it
gives multi-worker parity with the local backend without a socket. Not for
pods — each pod has its own ephemeral disk.
RedisSharedStorage — KV on Redis strings, pub/sub on a Redis Stream per
topic (an atomic INCR+XADD script keeps sequences ordered; MAXLEN bounds
the replay window; a subscriber past the trimmed floor gets SharedStorageGap).
Redis is the single source of truth, so no owner election. Pass a url
(defaults to $REDIS_URL) or an existing client to reuse a connection pool.
This is the only backend correct for horizontally-scaled, multi-pod
deployments.
The default LocalSharedStorage is per-container: its socket/loopback
election only reaches processes in the same network + filesystem namespace.
- Single process / single pod (e.g. Plotly Cloud apps): fine — one owner, nothing to fragment.
- Multiple gunicorn workers in one container: fine — they share the pod's loopback/socket and elect one owner.
- Multiple pods behind a load balancer (e.g. Dash Enterprise apps scaled by
an HPA, routed round-robin with no session affinity): each pod elects its own
isolated owner, so
set()/publish()on one pod are invisible on another. This works at 1 pod and fragments silently once it scales — useRedisSharedStorage(one Redis shared by all pods) instead.
BaseSharedStorage is the stable, public extension point. A backend — shipped
in-tree or as a separate package — implements:
get(key, default)/set(key, value)/delete(key)— JSON-compatible valuespublish(topic, message)andsubscribe(topic, replay_from=None) -> Subscription- optional
start()/close()(idempotent, called once per worker)
and returns a Subscription (__iter__ / __aiter__ / close) that raises
SharedStorageGap when the replay buffer overran. That trio —
BaseSharedStorage, Subscription, SharedStorageGap — is exported from
top-level dash; the poll-loop helpers (PollResult, the polling subscription)
are private and not part of the contract.
Core only ships backends whose dependency is already a Dash extra (msgspec
base; dash[diskcache]; dash[redis]). Anything needing a heavier dependency
belongs out of tree behind this same interface — e.g. a Postgres backend
(LISTEN/NOTIFY for push pub/sub + a table for KV and replay) lives in its own
package so a psycopg connection is never pulled into Dash core. It plugs in the
same way as a built-in: Dash(shared_storage=PostgresSharedStorage(...)).
| File | Responsibility |
|---|---|
_shared_storage/base.py |
Abstract interface: BaseSharedStorage, Subscription, SharedStorageError, SharedStorageGap |
_shared_storage/_engine.py |
StoreEngine: authoritative in-memory kv map + per-topic ordered log with bounded replay buffer (thread-safe, transport-agnostic) |
_shared_storage/local.py |
LocalSharedStorage: owner election; owner hosts the engine, clients proxy |
_shared_storage/diskcache.py |
DiskcacheSharedStorage: KV + append-log pub/sub on a diskcache.Cache |
_shared_storage/redis.py |
RedisSharedStorage: KV + Redis Streams pub/sub (atomic INCR+XADD) |
_shared_storage/_polling.py |
PollingSubscription: shared poll-loop subscription for the diskcache/Redis backends |
_shared_storage/_transport.py |
Length-prefixed, token-gated socket transport (local backend) |
_shared_storage/_codec.py |
msgspec msgpack codec (data-only) |
dash.py |
shared_storage constructor arg + lazy app.shared_storage property |
_callback_context.py |
dash.ctx.shared_storage accessor |
Requires msgspec (in requirements/install.txt); DiskcacheSharedStorage
needs the diskcache extra and RedisSharedStorage the redis extra.
Dash supports async def callbacks for non-blocking execution.
With Flask backend:
pip install dash[async]Async is auto-enabled when asgiref is detected. Or explicitly:
app = Dash(__name__, use_async=True)With Quart or FastAPI backend: Async is native - no extra dependencies needed.
from fastapi import FastAPI
from dash import Dash
server = FastAPI()
app = Dash(__name__, server=server) # Async works automaticallyimport asyncio
@app.callback(Output('output', 'children'), Input('input', 'value'))
async def async_update(value):
await asyncio.sleep(1) # Non-blocking
return f"Processed: {value}"- Regular async callbacks are non-blocking - multiple can run concurrently
- Background callbacks also support
async def - Jupyter uses
nest_asynciofor event loop compatibility - With Flask backend: requires
dash[async], coroutines raise error without it - With Quart/FastAPI backends: async is native, no extra setup needed
@app.callback(
Output('result', 'children'),
Input('btn', 'n_clicks'),
background=True,
manager=diskcache_manager,
)
async def async_background(n_clicks):
await asyncio.sleep(5)
return "Done"Both DiskcacheManager and CeleryManager support async functions via asyncio.run().
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.
- FastAPI backend required: WebSocket callbacks only work with FastAPI
- SharedWorker support: Modern browsers (not IE)
Enable globally for all callbacks:
from fastapi import FastAPI
from dash import Dash
server = FastAPI()
app = Dash(__name__, server=server, websocket_callbacks=True)Enable per-callback:
@app.callback(
Output('output', 'children'),
Input('input', 'value'),
websocket=True # Use WebSocket for this callback only
)
def update(value):
return f"Value: {value}"app = Dash(
__name__,
server=server,
websocket_callbacks=True,
websocket_inactivity_timeout=300000, # 5 minutes (default)
websocket_heartbeat_interval=30000, # 30 seconds (default)
websocket_allowed_origins=['https://example.com'],
)websocket_callbacks- Enable WebSocket for all callbacks (default:False)websocket_inactivity_timeout- Close WebSocket after period of inactivity in milliseconds (default:300000= 5 minutes). Heartbeats do not count as activity. Set to0to disable timeout. Connection automatically reconnects when needed.websocket_heartbeat_interval- Interval for heartbeat/keep-alive checks in milliseconds (default:30000= 30 seconds). Also determines how frequently inactivity timeout is checked.websocket_allowed_origins- List of allowed origins for WebSocket connections (security)
┌─────────────────────────────────────────────────────────────────────────┐
│ Browser Tab 1 Browser Tab 2 │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ Renderer │ │ Renderer │ │
│ └──────┬──────┘ └──────┬──────┘ │
│ │ postMessage │ postMessage │
│ └────────────┬───────────────────────┘ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ SharedWorker │ (one per origin) │
│ │ dash-ws-worker │ │
│ └──────────┬──────────┘ │
└────────────────────│────────────────────────────────────────────────────┘
│ WebSocket
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ Server (FastAPI) │
│ WebSocket Endpoint: /_dash-ws-callback │
└─────────────────────────────────────────────────────────────────────────┘
Connection & Reconnection Flow:
Renderer SharedWorker Server
│ │ │
│──[CONNECT]──────────────────>│ │
│ │──[WebSocket Connect]──>│
│<─[CONNECTED]─────────────────│<─[Connected]───────────│
│ │ │
│──[CALLBACK_REQUEST]─────────>│──[callback request]───>│
│<─[CALLBACK_RESPONSE]─────────│<─[callback response]───│
│ │ │
│ (inactivity) │ (heartbeat check) │
│ │──[close 4001]─────────>│
│<─[DISCONNECTED]──────────────│ │
│ │ │
│──[CALLBACK_REQUEST]─────────>│──[reconnect + send]───>│
│<─[CALLBACK_RESPONSE]─────────│<─[response]────────────│
- SharedWorker: Single WebSocket connection shared across browser tabs
- Heartbeat: Periodic ping/pong to detect dead connections (30s interval)
- Inactivity timeout: Closes connection after no actual callback activity (not heartbeats)
- Auto-reconnect: Reconnects automatically when a callback is triggered after timeout
WebSocket callbacks can stream updates to the client during execution using set_props() and read current component values using ctx.websocket:
import asyncio
from dash import callback, Output, Input, set_props, ctx
from dash.exceptions import PreventUpdate
@callback(
Output('result', 'children'),
Input('start-btn', 'n_clicks'),
prevent_initial_call=True
)
async def long_running_task(n_clicks):
ws = ctx.websocket
if not ws:
return "WebSocket not available"
# Stream progress updates to the client
for i in range(100):
# IMPORTANT: Check is_shutdown in loops to detect disconnections
if ws.is_shutdown:
raise PreventUpdate # Exit gracefully on disconnect
await asyncio.sleep(0.1)
set_props('progress-bar', {'value': i + 1})
set_props('status', {'children': f'Processing step {i + 1}/100...'})
# Read current value from another component
current_value = await ws.get_prop('input-field', 'value')
return f"Completed! Input was: {current_value}"IMPORTANT - Checking is_shutdown in Loops:
Long-running callbacks that use loops must check ws.is_shutdown to detect when the WebSocket connection has closed. Without this check:
- Callbacks continue running after the client disconnects, wasting server resources
set_propscalls go to a closed connection and are lost- The callback result is never delivered to the client
Only "persistent callbacks" (callbacks with no Output and no Input that use only set_props) are automatically restarted when the WebSocket reconnects. Regular callbacks with outputs are not restarted.
API:
set_props(component_id, props_dict)- Stream prop updates immediately to clientctx.websocket- Get WebSocket interface (returnsNoneif not in WS context)ws.is_shutdown- Check if the WebSocket connection has been closedawait ws.get_prop(component_id, prop_name)- Read current prop value from clientawait ws.set_prop(component_id, prop_name, value)- Set single prop (async version)await ws.close(code, reason)- Close the WebSocket connection
Use hooks to validate connections and messages:
from dash import Dash, hooks
@hooks.websocket_connect()
async def validate_connection(websocket):
"""Validate WebSocket connection before accepting."""
session_id = websocket.cookies.get("session_id")
if not session_id:
return (4001, "No session cookie")
if not await is_valid_session(session_id):
return (4002, "Invalid session")
return True # Allow connection
@hooks.websocket_message()
async def validate_message(websocket, message):
"""Validate each WebSocket message."""
session_id = websocket.cookies.get("session_id")
if not await is_session_active(session_id):
return (4002, "Session expired")
return True # Allow messageHook Return Values:
True(or truthy) - Allow connection/messageFalse- Reject with default code (4001)(code, reason)- Reject with custom close code and reason
dash/dash.py- WebSocket config in_generate_config()dash/dash-renderer/src/utils/workerClient.ts- Browser-side SharedWorker client@plotly/dash-websocket-worker/src/WebSocketManager.ts- WebSocket connection management@plotly/dash-websocket-worker/src/worker.ts- SharedWorker entry pointdash/backends/_fastapi.py- Server-side WebSocket handler
Dash automatically sanitizes dangerous URLs in components:
- Blocked protocols:
javascript:,vbscript: - Protected attributes:
href,src,action,formAction - Dangerous URLs replaced with
about:blank
Components with URL sanitization: html.A, html.Form, html.Iframe, html.Embed, html.Object, html.Button.
Generate hashes for inline scripts to use with CSP middleware:
from flask_talisman import Talisman
Talisman(app.server, content_security_policy={
"default-src": "'self'",
"script-src": ["'self'"] + app.csp_hashes()
})app.csp_hashes(hash_algorithm='sha256') returns base64-encoded hashes.
suppress_callback_exceptions=False(default) - Validates all callback IDs exist in layoutprevent_initial_callbacks=True- Prevents callbacks firing on page load (can also set per-callback withprevent_initial_call)
Meta tag values are HTML-escaped to prevent injection:
app = Dash(__name__, meta_tags=[
{"name": "description", "content": "Safe <content>"}
])dash/dash-renderer/src/utils/clientsideFunctions.ts- URL sanitization (clean_url)dash/dash.py:csp_hashes()- CSP hash generationtests/integration/security/- Security test coverage