gr.mount_gradio_app() — put a Gradio UI inside your FastAPI app

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.

What it does

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().

Why it matters

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.

Examples

# 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 /dashboard
Verified 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 demo
Not 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.

Flags

FlagMeaning
app / blocks / pathThe required trio: your FastAPI (or Starlette) app, the Blocks app, and the URL prefix to mount at.
authSame semantics as launch(): a tuple, list of tuples, or callable — checked before any component or /config request under the prefix.
theme / css / js / headGradio 6 moved app-wide look-and-feel to launch() — and mount_gradio_app takes them the same way, as parameters.
root_pathSet 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_sizeFile-serving controls for the mounted app's /file= routes and upload size cap, identical to launch().
ssr_mode / node_server_name / node_portServer-side rendering knobs; SSR needs a Node sidecar port, so in constrained containers set ssr_mode=False explicitly.
mcp_serverSince 5.28 the mounted app can expose MCP tools too — the LLM-agent door works under a prefix.

The FastAPI integration lands (2021)

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 (Nov 21, 2025): the launch() parameter surface unifies

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.

Under the hood: what mounting actually does

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.

Fun facts

Pros

Cons

Takeaways