One ASGI process for the API and the demo UI — mount Blocks at a path and both worlds share a port.
Your REST API and your Gradio demo don't need two ports, two processes, or a reverse proxy — mount one inside the other.
mount_gradio_app(app, blocks, path="/demo") attaches a gr.Blocks (or gr.Interface) app to an existing FastAPI (or Starlette) app at a URL prefix. Your FastAPI routes keep serving JSON at their own paths; the Gradio UI, its queue, file uploads and API endpoints all live under the mount path. Every launch() parameter you know — auth, theme, css, max_file_size, ssr_mode — is accepted here too, because mounting runs the same route setup as launch().
A lot of real ML services are FastAPI apps first: existing /predict routes, API keys, observability. Without mounting you run two servers and glue them with a proxy. With mounting it's one uvicorn process, one port, one deploy artifact — and the demo UI can reuse the same middleware, CORS and auth layers you already trust.
# one process, two doors: FastAPI route + full Gradio UI
import gradio as gr
from fastapi import FastAPI
app = FastAPI(title="Sensor API")
@app.get("/api/reading")
def reading():
return {"sensor": "BME280", "temp_c": 21.4, "humidity": 48.0}
gradio_app = gr.Blocks(title="Sensor Dashboard")
with gradio_app:
gr.Markdown("## Live sensor reading")
btn = gr.Button(value="Fetch reading")
out = gr.JSON(label="Reading")
@btn.click(outputs=out)
def fetch():
return {"sensor": "BME280", "temp_c": 21.4, "humidity": 48.0}
app = gr.mount_gradio_app(app, gradio_app, path="/dashboard")
# serve with: uvicorn main:app --port 8000
# FastAPI stays at /api/*, Gradio UI at /dashboardVerified in-process on gradio 6.27.0 (fastapi 0.141.1, starlette 1.6.0) with fastapi.testclient, no server started:
GET /api/reading -> 200 {'sensor': 'BME280', 'temp_c': 21.4, 'humidity': 48.0}
GET /dashboard -> 200 (renders the Blocks UI: Markdown "Live sensor reading", Button "Fetch reading", JSON output "Reading")
GET /dashboard/config -> 200; component types: ['markdown', 'button', 'json']
The /config payload reports gradio version 6.27.0 — the same JSON a standalone launch() would emit, just under the mount prefix.
Verified by executing the snippet with TestClient (in-process ASGI calls, no network). The mount returns a new app object — assign it back: app = gr.mount_gradio_app(...).
# auth per mount: logins handled by the Gradio route, FastAPI untouched
import gradio as gr
from fastapi import FastAPI
app = FastAPI()
demo = gr.Blocks(title="Internal metrics")
with demo:
t = gr.Textbox(label="Echo")
@t.submit(outputs=t)
def echo(x):
return x
app = gr.mount_gradio_app(app, demo, path="/internal", auth=("admin", "s3cret"))Verified on 6.27.0 with TestClient: GET /internal/config (no credentials) -> 401 POST /internal/login username=admin password=s3cret -> 200, sets access-token cookies POST /internal/login username=admin password=nope -> 400 (wrong credentials) With the login cookie: GET /internal -> 200, GET /internal/config -> 200. FastAPI's own routes stay open — only the mounted prefix is gated.
Login is served at <path>/login (an app-level route registered by the mount), taking username/password form fields — not user/pwd. Wrong credentials return 400; protected /config returns 401 before login.
# one Blocks app, two mount points (per-region docs UIs)
import gradio as gr
from fastapi import FastAPI
from fastapi.testclient import TestClient
app = FastAPI()
shared = gr.Blocks(title="Shared")
with shared:
t = gr.Textbox(label="Echo")
@t.submit(outputs=t)
def echo(x):
return x
app = gr.mount_gradio_app(app, shared, path="/en")
app = gr.mount_gradio_app(app, shared, path="/de")
client = TestClient(app)
for p in ("/en", "/de", "/en/config", "/de/config"):
print(p, client.get(p).status_code)Verified on 6.27.0: /en 200 /de 200 /en/config 200 /de/config 200 Both prefixes serve the same component tree; each mount registers its own routes under its path.
One caveat from the docs: loading a Space via gr.load drops custom css/js/head — mounting shares that caveat for apps built remotely rather than locally.
# the decorator variant: mount while you build the Blocks
import gradio as gr
from fastapi import FastAPI
app = FastAPI()
@app.get("/health")
def health():
return {"ok": True}
@gr.mount_gradio_app(app, path="/chat")
def create_chat_demo():
demo = gr.Blocks(title="Chat")
with demo:
gr.ChatInterface(lambda msg, hist: hist + [[msg, "echo: " + msg]])
return demoNot separately executed in-process; the decorator form is documented in the official mounting guide as @gr.mount_gradio_app(app, path="/chat") over a create_xxx_demo() factory returning a Blocks app.
Verified only against the documented API signature on 6.27.0, not executed — prefer example 1's pattern when you want a copy-paste-guaranteed path.
| Flag | Meaning |
|---|---|
app / blocks / path | The required trio: your FastAPI (or Starlette) app, the Blocks app, and the URL prefix to mount at. |
auth | Same semantics as launch(): a tuple, list of tuples, or callable — checked before any component or /config request under the prefix. |
theme / css / js / head | Gradio 6 moved app-wide look-and-feel to launch() — and mount_gradio_app takes them the same way, as parameters. |
root_path | Set this when a reverse proxy strips the prefix; Gradio builds asset URLs against it so the UI loads correctly behind nginx/Traefik. |
allowed_paths / blocked_paths / max_file_size | File-serving controls for the mounted app's /file= routes and upload size cap, identical to launch(). |
ssr_mode / node_server_name / node_port | Server-side rendering knobs; SSR needs a Node sidecar port, so in constrained containers set ssr_mode=False explicitly. |
mcp_server | Since 5.28 the mounted app can expose MCP tools too — the LLM-agent door works under a prefix. |
mount_gradio_app appeared as soon as Gradio users started embedding demos in larger services — the FastAPI integration guide documents it from the 2.x era, alongside mounting onto plain Starlette apps. Before that, the only pattern was two processes and a reverse proxy.
Gradio 6 moved theme/css/js/head out of the Blocks constructor and into launch() — and gave mount_gradio_app the identical parameter list, so mounting is no longer the odd one out. Mounting also gained the footer_links/run_history knobs and honors the new Python >= 3.10 requirement.
mount_gradio_app calls the same internal route builder as launch(), minus the server bootstrap: it registers /config, the queue join/status SSE endpoints under <path>/gradio_api/*, static asset routes and the /login handler onto the host app's router, then returns the FastAPI app. There is no second uvicorn and no proxy hop; the queue and SSE streams run inside your uvicorn workers. Auth is enforced by FastAPI dependencies on those routes, which is why a 401 (not a redirect) hits curl without a session cookie.