The ASGI Scalpel: Inside starlette-hot-reload

How a zero-dependency middleware uses Server-Sent Events to bring Vite-like frontend updates to Python.

6 min read • View on GitHub • More from pyk

A split illustration showing a massive drop forge crushing a gear on the left, and a precision laser modifying a gear on the right. This represents the difference between a full server reload and targeted asset injection.
Uvicorn's full-process reload acts like a drop forge. Asset-specific hot reloading acts like a precision laser.
Key Takeaways

The Full-Process Penalty

Python web developers using Jinja2 or FastAPI suffer from slow frontend feedback loops. Changing a simple CSS hex code traditionally requires Uvicorn to restart the entire backend process. This is a sledgehammer for a thumbtack. Dropping state and rebooting database connections slows development to a crawl.

Rejecting the WebSocket Default

Most live-reloaders default to WebSockets. This introduces heavy bidirectional communication and extra dependencies. The creator of starlette-hot-reload chose Server-Sent Events (SSE) instead. SSE provides a unidirectional standard HTTP protocol perfectly suited for a server notifying a client.

SolutionReload ScopeProtocolDependencies
Uvicorn --reloadFull ServerFile WatcherNone
ArelBrowser OnlyWebSocketswebsockets library
starlette-hot-reloadBrowser OnlySSENone

Surgical ASGI Injection

The technical core relies on bypassing Starlette's BaseHTTPMiddleware. That standard class introduces context variable overhead and performance penalties. Instead, the project uses a raw ASGI callable to intercept the byte stream.

The ASGI interceptor buffers the HTML body, injects the script tag before the closing body tag, and dynamically rewrites the content-length header.

The CSS Cache-Busting Trick

The injected client script contains smart update logic. When an SSE 'css' event arrives, the script finds all stylesheet link tags and appends a timestamp query parameter. This forces the browser to re-fetch the CSS without a jarring page refresh.

if (event.data === 'css') {
  const links = document.querySelectorAll('link[rel="stylesheet"]');
  links.forEach(link => {
    const url = new URL(link.href);
    url.searchParams.set('_hot_reload', Date.now());
    link.href = url.toString();
  });
}