Pluggable Backends
Run this documentation site on Flask, FastAPI or Quart with one environment variable, and learn what each backend unlocks.
Overview
Dash 4.1+ introduces a pluggable server backend. Instead of being hard-wired to Flask, a Dash app can now be created on top of FastAPI or Quart, unlocking native async, websocket callbacks, and a transport surface that works well with the new MCP tooling shipping in Dash 4.3.
This boilerplate exposes the choice through a single environment variable so you can flip backends without editing code:
# Default — fully featured, all integrations on
DASH_BACKEND=flask python run.py
# Async backends (install the extra first)
pip install "dash[fastapi]"
DASH_BACKEND=fastapi python run.py
pip install "dash[quart]"
DASH_BACKEND=quart python run.py
The badge in the header and the navbar reflects whichever backend booted, so you always know what's running.
How it's wired
The active backend is resolved once in lib/backend.py and threaded into both run.py and the UI components:
# File: lib/backend.py
"""
Backend selection helper for the Dash documentation boilerplate.
Dash 4.1+ supports pluggable backends:
app = Dash(backend="flask" | "fastapi" | "quart")
This module owns the single source of truth for which backend the app is
running on, so both `run.py` and UI components (e.g. the navbar badge) stay
in sync.
Backend is selected via the ``DASH_BACKEND`` environment variable. Falls back
to ``flask`` if unset or invalid.
"""
from __future__ import annotations
import os
from dataclasses import dataclass
from typing import Literal
try:
from dotenv import load_dotenv
load_dotenv()
except ImportError:
pass
BackendName = Literal["flask", "fastapi", "quart"]
SUPPORTED_BACKENDS: tuple[BackendName, ...] = ("flask", "fastapi", "quart")
DEFAULT_BACKEND: BackendName = "flask"
@dataclass(frozen=True)
class BackendInfo:
name: BackendName
label: str
color: str # DMC color name
icon: str # iconify icon id
description: str
is_async: bool
_BACKEND_INFO: dict[str, BackendInfo] = {
"flask": BackendInfo(
name="flask",
label="Flask",
color="gray",
icon="simple-icons:flask",
description="WSGI backend. Default. Full feature parity with Dash 3.x.",
is_async=False,
),
"fastapi": BackendInfo(
name="fastapi",
label="FastAPI",
color="teal",
icon="simple-icons:fastapi",
description="ASGI backend. Adds websocket callbacks, async support and MCP-friendly transport.",
is_async=True,
),
"quart": BackendInfo(
name="quart",
label="Quart",
color="violet",
icon="simple-icons:python",
description="ASGI backend with a Flask-compatible API surface.",
is_async=True,
),
}
def resolve_backend(name: str | None = None) -> BackendName:
"""Resolve the backend to use.
Reads ``DASH_BACKEND`` env var when ``name`` is not given. Unknown values
fall back to the default backend rather than raising — the docs site
should always boot.
"""
raw = (name or os.environ.get("DASH_BACKEND") or DEFAULT_BACKEND).strip().lower()
if raw not in SUPPORTED_BACKENDS:
return DEFAULT_BACKEND
return raw # type: ignore[return-value]
def get_backend_info(name: str | None = None) -> BackendInfo:
return _BACKEND_INFO[resolve_backend(name)]
run.py then passes the resolved name straight to the Dash() constructor:
from lib.backend import resolve_backend, get_backend_info
BACKEND = resolve_backend()
BACKEND_INFO = get_backend_info(BACKEND)
IS_FLASK = BACKEND == "flask"
app = Dash(
__name__,
backend=BACKEND, # <-- new in Dash 4.1
suppress_callback_exceptions=True,
use_pages=True,
...
)
app._backend_info = BACKEND_INFO
The only remaining Flask-only integration is the before_request analytics hook, which the boilerplate transparently swaps for a Starlette BaseHTTPMiddleware when the backend is FastAPI. add_llms_routes(app) from dash-improve-my-llms 2.0 auto-detects the backend and mounts its own router under all three — no IS_FLASK gate is needed for the AI/LLM surfaces anymore.
What you get on each backend
| Feature | Flask | FastAPI | Quart |
|---|---|---|---|
| Synchronous callbacks | yes | yes | yes |
async def callbacks | n/a | yes | yes |
Websocket callbacks (ctx.websocket) | no | yes (Dash 4.2+) | yes (Dash 4.2+) |
| Persistent callbacks (Dash 4.2 rc) | no | yes | yes |
MCP server / mcp_enabled decorator (Dash 4.3) | partial | yes | yes |
add_llms_routes (dash-improve-my-llms 2.0) | yes | yes | yes |
/llms.txt, /robots.txt, /sitemap.xml | yes | yes | yes |
gunicorn deploy | yes | n/a | n/a |
uvicorn deploy | n/a | yes | yes |
The AI/LLM surfaces (/llms.txt, /robots.txt, /sitemap.xml, bot middleware, MCP bridge) work identically under all three backends — pick the one that matches the rest of your stack.
Choosing a backend
Flask (default)
The safe choice. Everything in this boilerplate works without changes, including the analytics tracker, the dash-improve-my-llms integration, and the SEO routes.
Deploy with gunicorn run:server -b 0.0.0.0:8550 (already wired into the included Dockerfile).
FastAPI
Pick this when you want:
- Async callbacks (
async def update_table(...)) - Real-time websocket callbacks for streaming UI updates
- A first-class MCP server using Dash 4.3's framework utilities
- Easy interop with an existing FastAPI app — just pass it in:
from fastapi import FastAPI
from dash import Dash
api = FastAPI()
api.add_api_route("/healthz", lambda: {"ok": True})
app = Dash(__name__, server=api) # Dash detects FastAPI automatically
Deploy with uvicorn run:server --host 0.0.0.0 --port 8550.
Quart
A drop-in option for teams already on Quart, or who want async with Flask-style ergonomics (@server.before_request becomes @server.before_request returning a coroutine).
from quart import Quart
from dash import Dash
server = Quart(__name__)
app = Dash(__name__, server=server)
Deploy with uvicorn run:server --host 0.0.0.0 --port 8550.
Custom backends
Dash(backend=...) also accepts a subclass of dash.backends.base_server.BaseDashServer plus matching request/response adapters, so internal frameworks (a hardened ASGI server, an in-process test harness, etc.) can be plugged in without forking Dash. See the dash.backends module for the protocol.
Migration checklist for your own apps
- Bump
dash>=4.1.0(anddash-mantine-components>=2.7.0if you use DMC). - Replace any
app.run_server(...)calls withapp.run(...). - Audit every
@app.server.before_request/@app.server.route— Flask-only. Wrap them in anif backend == "flask":guard or rewrite as FastAPI/Quart middleware. - Install the extra:
pip install "dash[fastapi]"orpip install "dash[quart]". - Pass
backend="fastapi"(or"quart") toDash(...), or setDASH_BACKENDif you're using this boilerplate's helper. - Swap your process manager:
gunicorn→uvicorn(orhypercorn).
What this site does today
This page is itself rendered by whichever backend DASH_BACKEND points at. The header badge tells you which one — change the env var, restart run.py, and the badge updates to match.
Source: /backends
Note for AI agents: This is the static, prerendered view of an interactive Dash application served because we detected a non-JS user agent. Full prose docs:
- /backends/llms.txt — LLM-friendly documentation
- /sitemap.xml
- /robots.txt