# Backend Deep Dive > An in-depth comparison of Flask, FastAPI and Quart as Dash 4.x backends — architecture, strengths, weaknesses, deployment, and concrete best practices for each. --- .. llms_copy::Backend Deep Dive .. toc:: ### Why this page exists Dash 4.1 ships with a real choice for the first time: ```python app = Dash(__name__, backend="flask") # WSGI, sync app = Dash(__name__, backend="fastapi") # ASGI, async, websockets, MCP app = Dash(__name__, backend="quart") # ASGI, async, Flask-style API ``` Each backend pulls a different runtime model under your Dash app. The choice changes what your callbacks can do, which third-party packages will keep working, and how you deploy to production. This page goes one layer deeper than the [Pluggable Backends](/backends) overview and lays out the **trade-offs per backend** so you can pick one with eyes open. --- ### Mental model: WSGI vs ASGI Before comparing backends, the single most important distinction is the protocol they speak to the web server. | | WSGI | ASGI | |--|--|--| | Stands for | Web Server Gateway Interface | Asynchronous Server Gateway Interface | | Concurrency | One request per worker thread | Many requests per event loop | | Native `async def` | No | Yes | | WebSockets | No (needs a separate stack) | Yes (first-class) | | HTTP/2, SSE, long-lived streams | Awkward / via extensions | Native | | Process manager | `gunicorn`, `mod_wsgi` | `uvicorn`, `hypercorn`, `daphne` | | CPU-bound work | Fine (sync) | Needs `run_in_executor` or background tasks | **Flask is WSGI. FastAPI and Quart are ASGI.** Everything else flows from that. --- ### Flask backend — the safe default #### Architecture - Pure WSGI synchronous server. - One worker thread handles one request at a time. - The entire Dash 3.x ecosystem and almost every third-party Dash extension assume Flask under the hood. #### Pros - **Mature ecosystem.** Every Flask extension (`Flask-Login`, `Flask-Caching`, `Flask-Limiter`, `Flask-Babel`, …) plugs in without translation. - **Predictable concurrency.** No reentrancy footguns; if your code is thread-safe in Flask 2/3 it's thread-safe under Dash. - **First-class for Dash addons.** `dash-improve-my-llms`, `dash-auth`, most observability hooks register through Flask's `before_request` / `after_request`. - **Cheapest to deploy.** `gunicorn` with sync or `gthread` workers is well understood and rock-solid. - **Easiest to debug.** Stack traces stop in your callback — no event-loop noise, no task scheduling. #### Cons - **No native async.** `async def` callbacks fall back to a thread pool, losing most of the benefit of being async. - **No websocket callbacks** (Dash 4.2's `ctx.websocket` is ASGI-only). - **No persistent callbacks** (Dash 4.2 RC). - **Limited MCP transport.** Dash 4.3's MCP framework uses Streamable HTTP, which prefers ASGI. - **Worker-per-request scaling.** Long-running callbacks tie up a worker; you need lots of them (or background callbacks) under load. #### When Flask is the right call - You already depend on Flask extensions and don't want to migrate. - Your callbacks are CPU-bound or short and synchronous. - You're shipping internal dashboards where two-digit RPS is plenty. - You want the maximum number of third-party Dash add-ons to "just work." - You're under deadline pressure — Flask is the path of least surprise. #### Best practices 1. **Use a process manager with multiple workers.** `gunicorn -w 4 -k gthread --threads 8 run:server` is a sensible default for I/O-bound dashboards. 2. **Push long work to background callbacks** (`background=True`, Celery / Redis Queue) — never block a worker for >1s if you can help it. 3. **Add `Flask-Caching`** for expensive layout or data calls. Set `dcc.Store(storage_type="memory")` plus server-side caching for big payloads. 4. **Use `before_request`/`after_request`** for cross-cutting concerns (auth, telemetry, response headers). This is exactly the pattern this boilerplate uses for analytics. 5. **Don't import `asyncio` in callbacks.** If you find yourself wanting to, you've outgrown Flask — switch to FastAPI or Quart. 6. **Pin `gunicorn` worker timeouts** higher than your slowest callback (`--timeout 60`), or split slow work into a background callback. #### Deployment recipe ```bash pip install "dash>=4.1.0" gunicorn gunicorn run:server \ -w 4 -k gthread --threads 8 \ -b 0.0.0.0:8550 --timeout 60 \ --access-logfile - --error-logfile - ``` --- ### FastAPI backend — async, websockets, MCP #### Architecture - ASGI server with native `async def`. - Dash hooks into FastAPI's routing and dependency injection. - `app.server` is a real `FastAPI` instance — every FastAPI feature (path operations, dependencies, OpenAPI, `Depends`, middleware) is available. ```python from fastapi import FastAPI from dash import Dash api = FastAPI(title="My App API") @api.get("/healthz") def healthz(): return {"ok": True} app = Dash(__name__, server=api) # Dash auto-detects FastAPI ``` #### Pros - **Native `async def` callbacks.** I/O-heavy callbacks (DB queries, HTTP fetches) can fan out without tying up a thread. - **Websocket callbacks** (Dash 4.2+). Stream live updates with `ctx.websocket` without bolting on Flask-SocketIO. - **Persistent callbacks** (Dash 4.2 RC). Long-lived server-side state tied to a user session. - **MCP-friendly.** Dash 4.3's MCP server exposes layout, components, and callbacks as tools over Streamable HTTP — best supported on ASGI. - **Free OpenAPI docs** for any non-Dash routes you add (`/docs`, `/redoc`). - **Excellent typing.** Pydantic v2 models cross-validate request/response without extra plumbing — pairs naturally with `pydantic` already in this stack. - **Modern observability.** Native compatibility with OpenTelemetry, Sentry's ASGI middleware, structlog, etc. #### Cons - **Newer, fewer Dash add-ons.** Packages that hard-code `app.server.before_request` (including `dash-improve-my-llms` today) need adaptation. - **Async footguns.** A blocking call (`requests.get`, `time.sleep`, sync DB driver) inside an `async def` callback freezes the entire event loop for that worker. - **Two mental models.** Sync and async callbacks mix in the same app — you must remember which is which. - **Deployment is `uvicorn`-shaped.** `gunicorn` is still usable as a process manager (`-k uvicorn.workers.UvicornWorker`), but the muscle memory is different. #### When FastAPI is the right call - You want **websocket callbacks** or **persistent callbacks** from Dash 4.2. - You're building an **MCP server** on top of your Dash app (Dash 4.3). - Your callbacks do **lots of network I/O** (databases, external APIs, search) and you want true concurrency. - You're already exposing a **JSON API alongside the dashboard** — FastAPI lets you do both in one process with one set of types. - You want **OpenAPI** documentation for free. #### Best practices 1. **Use async drivers everywhere.** `asyncpg` instead of `psycopg2`, `httpx.AsyncClient` instead of `requests`, `aioboto3` instead of `boto3`. One sync call inside an `async def` callback wipes out the benefits. 2. **Wrap unavoidable blocking calls** in `asyncio.to_thread(...)` or `loop.run_in_executor(...)` so they don't stall the event loop. 3. **Mark callbacks `async def` only when you'll actually `await` something.** A sync callback wrapped in `async def` adds overhead without benefit. 4. **Replace `@server.before_request` with ASGI middleware:** ```python @app.server.middleware("http") async def track_visitor(request, call_next): try: tracker.track_visit(request.url.path, request.headers.get("user-agent", ""), request.client.host) except Exception: pass return await call_next(request) ``` 5. **Run with `uvicorn`** in development and either `uvicorn` workers or `hypercorn` in production: ```bash pip install "dash[fastapi]" "uvicorn[standard]" uvicorn run:server --host 0.0.0.0 --port 8550 --workers 4 ``` 6. **Health-check the event loop**, not just the process. A blocked loop will pass a TCP health-check but serve nothing — use a `/healthz` route that returns the loop's latency. 7. **Use `BackgroundTasks`** (FastAPI primitive) for fire-and-forget work that doesn't need a callback round-trip. 8. **Watch out for `set_props`** — under ASGI it's near-instant, which is a feature, but multi-worker setups still need a shared message bus (Redis pub/sub) for cross-worker notifications. #### Deployment recipe ```bash pip install "dash[fastapi]" "uvicorn[standard]" uvicorn run:server --host 0.0.0.0 --port 8550 \ --workers 4 --log-level info \ --proxy-headers --forwarded-allow-ips='*' ``` For containerized deploys, replace `gunicorn run:server` in the Dockerfile with the `uvicorn` command above. --- ### Quart backend — async with Flask ergonomics #### Architecture - ASGI server with an **API surface almost identical to Flask** (`@server.route`, `@server.before_request`, `request.headers`, etc., but `async`). - Designed as a drop-in async replacement for Flask apps. ```python from quart import Quart from dash import Dash server = Quart(__name__) @server.before_request async def log_path(): print(server.request.path) app = Dash(__name__, server=server) ``` #### Pros - **Same decorators as Flask, but async.** Easiest migration path for teams who already use `before_request`, `after_request`, blueprints, etc. - **Native async + websockets** like FastAPI. - **Smaller API surface** than FastAPI — less to learn if you don't need OpenAPI / dependency injection. - Works with **Quart-Auth, Quart-CORS, Quart-Schema** — the Flask-style addon family but async. #### Cons - **Smallest ecosystem** of the three. Many Flask extensions don't have Quart equivalents yet. - **Less momentum than FastAPI.** Most new Dash features (MCP, websocket callbacks) are tested on FastAPI first. - **Same async footguns** as FastAPI — blocking calls freeze the loop. - Documentation and community examples are sparser; debugging unusual cases takes more effort. #### When Quart is the right call - You have an existing **Flask codebase** with custom middleware and want async without rewriting every route. - You want websocket callbacks **without** committing to FastAPI's larger surface (Pydantic-heavy, OpenAPI-by-default). - You like Flask's mental model — globals, decorators, blueprints — but need async I/O. #### Best practices 1. **Replace `requests` with `httpx`** (or `aiohttp`) — Quart will let you `await` it natively. 2. **Convert `before_request` to `async def`.** Same decorator, but the function must be a coroutine. 3. **Don't mix Flask-only extensions.** `Flask-Login` will fail under Quart; use `Quart-Auth` instead. 4. **Use `hypercorn` or `uvicorn`** as the ASGI server. ```bash pip install "dash[quart]" hypercorn hypercorn run:server --bind 0.0.0.0:8550 --workers 4 ``` 5. **Treat it as "Flask with `async`"** in code review — the syntactic similarity hides a different runtime model. Reviewers should explicitly ask "could this block the loop?" for every PR. #### Deployment recipe ```bash pip install "dash[quart]" "uvicorn[standard]" uvicorn run:server --host 0.0.0.0 --port 8550 --workers 4 ``` --- ### Side-by-side at a glance | Concern | Flask | FastAPI | Quart | |--|--|--|--| | Protocol | WSGI | ASGI | ASGI | | `async def` callbacks | Threaded | Native | Native | | Websocket callbacks (Dash 4.2) | no | yes | yes | | Persistent callbacks (Dash 4.2) | no | yes | yes | | MCP server (Dash 4.3) | partial | best support | yes | | Add-on compatibility | Highest | Growing | Smallest | | `dash-improve-my-llms` today | yes | not yet | not yet | | OpenAPI for sibling routes | no | built-in | optional (`quart-schema`) | | Easiest migration *from Flask* | n/a | medium | trivial | | Recommended process manager | `gunicorn` | `uvicorn` | `uvicorn` / `hypercorn` | | Suitable for high-RPS I/O | medium | high | high | | Maturity / risk | lowest | medium | medium | --- ### A decision flow 1. **Do you need websocket callbacks, persistent callbacks, or MCP today?** → FastAPI. 2. **Do you have a large existing Flask codebase you want to make async with minimal diff?** → Quart. 3. **Do you depend on `dash-improve-my-llms`, `Flask-Login`, or other Flask-only Dash add-ons?** → Flask. 4. **Are your callbacks I/O bound (DBs, HTTP, queues) and is throughput your bottleneck?** → FastAPI. 5. **Is this an internal tool that will see <50 concurrent users and you want zero surprises?** → Flask. 6. **Otherwise** → start on Flask, switch to FastAPI when you hit a feature wall. Dash makes the swap a one-line change. --- ### Cross-backend best practices These hold no matter which backend you pick. 1. **Don't put `app.server.route` decorations in the hot path of `run.py`.** Group them into a small `register_extra_routes(server, backend)` function so you can gate it on backend type without making `run.py` unreadable. (This is what `if IS_FLASK:` does in this boilerplate's `run.py`.) 2. **Keep callbacks fast.** Anything over ~250ms hurts UX; over ~3s should be a background callback or `dcc.Loading`. 3. **Cache expensive layout work**, especially when `use_pages=True` — page registry traversals add up. 4. **Pin Python version** in deployment. Async semantics shifted noticeably between 3.10 and 3.11; debugging across versions is painful. 5. **Health-check separately from readiness-check.** Health = "process is up." Readiness = "DB / cache / dependent services are reachable." 6. **Log structured.** `structlog` works with all three backends and saves you when debugging a callback that only misbehaves on one of them. 7. **Don't mix `set_props` and pattern-matching callbacks** unless you've thought about the ordering — under FastAPI both fire faster than under Flask, exposing races you wouldn't see in development. 8. **Test on the backend you'll deploy on.** A test suite that only exercises Flask will miss async-only failures. --- ### Switching backends in this boilerplate Everything above is enforceable in one line — flip `DASH_BACKEND` in `.env`: ```env DASH_BACKEND=flask # default, full feature parity DASH_BACKEND=fastapi # async + websockets + MCP DASH_BACKEND=quart # async with Flask-style decorators ``` The badge in the header reflects the resolved backend. `dash-improve-my-llms` 2.0 now serves `/llms.txt`, `/robots.txt`, and `/sitemap.xml` natively under Flask, FastAPI, and Quart — only the analytics `before_request` hook is still Flask-only (an ASGI middleware port is wired up automatically under FastAPI). See [Pluggable Backends](/backends) for the implementation details. --- *Source: /backend-comparison*