2025, Dec 31 03:00

Run FastAPI and Gradio in one process: live API request logs in a Gradio admin dashboard via polling

Learn why FastAPI can't push updates to a Gradio UI in one process and how to fix it: poll with Textbox every to show real-time API logs in an admin panel.

Running FastAPI for an API and Gradio for an admin dashboard in the same process sounds convenient until you try to push data from the API handler directly into the Gradio UI. The classic case: you post to a FastAPI route, append a line to a shared log, and expect a Textbox in the Gradio panel to update in real time. It doesn’t. The reason is subtle but important for anyone coupling an API and a UI within one service.

Problem setup

The goal is simple: expose a FastAPI endpoint on one port, serve a Gradio admin panel on another, and display incoming HTTP requests as they arrive. The first attempt wires both into one script, shares a log buffer, and tries to nudge the Textbox from inside the FastAPI route.

import os
import gradio as gr
from fastapi import FastAPI, Request
import uvicorn
import threading
from typing import List
from datetime import datetime
http_api = FastAPI()
class EventLog:
    def __init__(self):
        self._lines: List[str] = []
        self.buffer = ""
    def add_record(self, message: str):
        timestamp = datetime.now().strftime("%H:%M:%S")
        self._lines.append(f"[{timestamp}] {message}")
        self.buffer = "\n".join(self._lines)
shared_log = EventLog()
shared_state = gr.State(shared_log)
@http_api.post("/log")
async def ingest_log(request: Request):
    body = await request.body()
    info = f"API received: {body}"
    shared_log.add_record(info)
    gr.update(value=shared_log.buffer)
    return {"status": "logged", "message": info}
def serve_http():
    api_port = int(os.environ.get("API_PORT", 8000))
    uvicorn.run(http_api, host="0.0.0.0", port=api_port)
with gr.Blocks() as panel:
    gr.Markdown("## Incoming HTTP Requests")
    log_view = gr.Textbox(label="Logs", inputs=shared_state, lines=20)
def serve_panel():
    ui_port = int(os.environ.get("GRADIO_PORT", 7860))
    panel.launch(server_port=ui_port)
if __name__ == "__main__":
    threading.Thread(target=serve_http, daemon=True).start()
    serve_panel()

The pipeline is intended to look like this: POST /log into FastAPI, append to a common store, and have Gradio show the latest text. However, changing the Textbox from the FastAPI handler does not update the browser.

Why the update doesn’t reach the browser

The UI runs in the user’s browser and is driven by Gradio’s frontend. When FastAPI handles a request, it’s operating in a different execution path from the Gradio UI event loop. A direct call like gr.update in the API route doesn’t push a state change to an already rendered component. Without a UI-triggered event or a client-initiated refresh, the page won’t re-render. In other words, the server-side mutation happens, but the client isn’t told to repaint.

Working approach: poll from the UI

The practical solution is to let the Gradio component poll the server-side function at a fixed interval. Gradio Textbox supports a callable for value and an every parameter that re-invokes that callable periodically. Returning the current log snapshot from that function gives you a live view, updated on a timer.

This method keeps FastAPI and Gradio loosely coupled and aligns with how the Gradio frontend expects data to change. It also fits the goal of serving an API and a separate admin UI in one executable, on two ports, for a mock service setup.

Solution code

Below is a complete example using a polling Textbox. The API appends to the shared log; the Gradio component calls a function that returns the current content every second.

import os
import gradio as gr
from fastapi import FastAPI, Request
import uvicorn
import threading
from typing import List
from datetime import datetime
http_api = FastAPI()
class EventLog:
    def __init__(self):
        self._lines: List[str] = []
        self.buffer = ""
    def add_record(self, message: str):
        timestamp = datetime.now().strftime("%H:%M:%S")
        self._lines.append(f"[{timestamp}] {message}")
        self.buffer = "\n".join(self._lines)
shared_log = EventLog()
shared_state = gr.State(shared_log)
@http_api.post("/log")
async def ingest_log(request: Request):
    body = await request.body()
    info = f"API received: {body}"
    shared_log.add_record(info)
    gr.update(value=shared_log.buffer)
    return {"status": "logged", "message": info}
def serve_http():
    api_port = int(os.environ.get("API_PORT", 8000))
    uvicorn.run(http_api, host="0.0.0.0", port=api_port)
def read_buffer():
    return shared_log.buffer
with gr.Blocks() as panel:
    gr.Markdown("## Incoming HTTP Requests")
    log_view = gr.Textbox(label="Logs", value=read_buffer, lines=20, every=1)
def serve_panel():
    ui_port = int(os.environ.get("GRADIO_PORT", 7860))
    panel.launch(server_port=ui_port)
if __name__ == "__main__":
    threading.Thread(target=serve_http, daemon=True).start()
    serve_panel()

This uses Textbox(value=read_buffer, every=1), which executes read_buffer every second and renders whatever string it returns. The docs for this behavior are in Gradio Textbox: https://www.gradio.app/docs/gradio/textbox.

Why this matters

When you package an API and a control panel together—especially for mocking third‑party services—you often want clean separation at the network level while sharing a bit of state in-process. FastAPI and Gradio can coexist like that: the API handles traffic on one port, the UI serves on another, and the UI pulls data on a timer. Understanding that the UI is client-driven helps avoid dead-ends where server-side calls try to push updates into a browser without a corresponding UI event.

Takeaways

If you need a Gradio component to reflect state that’s changed outside the UI’s own callbacks, rely on a callable value and the every parameter to poll. Keep the logging structure simple, return the current snapshot, and let the frontend refresh on schedule. For the described use case—separate admin panel and API in one deployment—this pattern is straightforward and effective.