# How the VoiceStudio Events Router Streams SSE Updates to the React Frontend

> Learn how VoiceStudio streams SSE updates to React using FastAPI's StreamingResponse and the EventSource API for real-time job progress, logs, and notifications with auto-reconnect.

- Repository: [Palash Debnath/VoiceStudio](https://github.com/debpalash/VoiceStudio)
- Tags: internals
- Published: 2026-09-13

---

**VoiceStudio implements a dual-transport real-time system where FastAPI endpoints in [`backend/api/routers/dub_core.py`](https://github.com/debpalash/VoiceStudio/blob/main/backend/api/routers/dub_core.py) and related modules return `StreamingResponse` objects with `media_type="text/event-stream"`, while the React frontend consumes these Server-Sent Events (SSE) via the native `EventSource` API to receive job progress, logs, and system notifications with automatic reconnection support.**

The VoiceStudio repository (`debpalash/VoiceStudio`) bridges its Python backend and React frontend through a sophisticated event streaming architecture designed for reliability across unstable networks. At its core, the **SSE (Server-Sent Events)** routing mechanism streams structured JSON data over persistent HTTP connections, utilizing sequence-number-based persistence to ensure no updates are lost during client reconnections. This design supports high-latency tolerance while delivering real-time transcription progress, model download status, and system diagnostics to the UI.

## Backend SSE Architecture and Event Persistence

The SSE implementation decouples event generation from transmission through a centralized persistent store. When background tasks execute—such as speaker diarization or HuggingFace model downloads—throughout the VoiceStudio pipeline, the code invokes `job_store.append_event(job_id, payload)` defined in **[`backend/core/job_store.py`](https://github.com/debpalash/VoiceStudio/blob/main/backend/core/job_store.py)**. This method stores raw SSE-formatted strings (`data: {...}\n\n`) alongside monotonically increasing sequence identifiers.

To support client recovery after network interruptions, the store exposes `job_store.events_since(job_id, after_seq)`. This function retrieves all events recorded after a specified sequence number, enabling the React frontend to resume streams exactly where they disconnected without data loss.

### SSE Endpoint Implementation with StreamingResponse

Individual FastAPI routers expose SSE endpoints using asynchronous generators wrapped in `StreamingResponse`. In **[`backend/api/routers/dub_core.py`](https://github.com/debpalash/VoiceStudio/blob/main/backend/api/routers/dub_core.py)** (lines 592-718), the `/jobs/{job_id}/events` route implements an async generator that constructs properly formatted SSE protocol lines:

```python

# Conceptual implementation from backend/api/routers/dub_core.py

async def event_generator(job_id: str, after_seq: int):
    # Replay persisted events from job_store first

    for event in job_store.events_since(job_id, after_seq):
        yield f"data: {json.dumps(event)}\n\n"
    
    # Continue yielding real-time events as they arrive...

```

The generator utilizes helper functions (such as the `_line` utility referenced in the dub_core implementation) to ensure compliance with the SSE specification. Each yielded string follows the `data: <json_payload>\n\n` format required for browser `EventSource` compatibility.

### Keep-Alive Mechanism

To prevent intermediary proxies from closing idle connections, VoiceStudio emits periodic SSE comment lines. When no new events arrive within a configurable interval, the generator sends `:keepalive\n\n`—a comment line per the SSE specification that browsers ignore but which maintains the TCP connection. This pattern appears in **[`backend/api/routers/events.py`](https://github.com/debpalash/VoiceStudio/blob/main/backend/api/routers/events.py)** (line 44) and throughout the SSE streaming implementations.

## WebSocket Alternative for UI-Wide Events

While SSE handles job-specific unidirectional streams, VoiceStudio employs WebSocket connections for application-wide sidebar notifications. The **[`backend/api/routers/events.py`](https://github.com/debpalash/VoiceStudio/blob/main/backend/api/routers/events.py)** module defines `@router.websocket("/ws/events")`, which subscribes connecting clients to a global event broadcaster. This bidirectional channel uses `await ws.send_text(json.dumps(event))` to push JSON-formatted UI updates that do not require the persistence guarantees of job progress tracking.

## React Frontend Integration

The React application selects between transport protocols based on data criticality and directionality, implementing distinct hooks for each stream type.

### Consuming SSE via EventSource

For job-specific progress tracking, the frontend utilizes the browser's native `EventSource` API. The implementation establishes connections to endpoints like `/jobs/${jobId}/events` and manages reconnection logic through sequence number synchronization:

```typescript
const useJobEvents = (jobId: string) => {
  const [events, setEvents] = React.useState<Array<any>>([]);
  const [lastSeq, setLastSeq] = React.useState(0);

  React.useEffect(() => {
    const source = new EventSource(
      `${process.env.REACT_APP_API_URL}/jobs/${jobId}/events?after_seq=${lastSeq}`
    );

    source.onmessage = (e) => {
      const ev = JSON.parse(e.data);
      setEvents((prev) => [...prev, ev]);
      if (ev.seq) setLastSeq(ev.seq);
    };
    
    source.onerror = () => source.close();

    return () => source.close();
  }, [jobId, lastSeq]);

  return events;
};

```

This hook demonstrates **sequence number synchronization**, a critical reliability feature. By tracking `lastSeq` and appending it as the `after_seq` query parameter, the client ensures `job_store.events_since()` replays exactly the missed events following a network disruption.

### WebSocket Implementation for Global Events

For the sidebar event stream, the React application instantiates a `WebSocket` object targeting the endpoint defined in [`backend/api/routers/events.py`](https://github.com/debpalash/VoiceStudio/blob/main/backend/api/routers/events.py):

```typescript
React.useEffect(() => {
  const ws = new WebSocket(`${process.env.REACT_APP_API_URL}/ws/events`);
  
  ws.onmessage = (e) => {
    const ev = JSON.parse(e.data);
    // Dispatch to Redux store or local state management
  };
  
  return () => ws.close();
}, []);

```

## Key Source Files and Functions

| File Path | Purpose | Key Functions/Decorators |
|-----------|---------|-------------------------|
| [`backend/api/routers/dub_core.py`](https://github.com/debpalash/VoiceStudio/blob/main/backend/api/routers/dub_core.py) | Job-specific SSE streaming | `_line` helper, `_stream_events` generator, `/jobs/{job_id}/events` route (lines 592-718, 876-888) |
| [`backend/api/routers/system.py`](https://github.com/debpalash/VoiceStudio/blob/main/backend/api/routers/system.py) | System log SSE streaming | `/system/logs/stream` returning `StreamingResponse` (lines 520-576) |
| [`backend/api/routers/setup/download.py`](https://github.com/debpalash/VoiceStudio/blob/main/backend/api/routers/setup/download.py) | Model download progress SSE | HF download tqdm integration (lines 255-306, 432-433) |
| [`backend/api/routers/events.py`](https://github.com/debpalash/VoiceStudio/blob/main/backend/api/routers/events.py) | WebSocket endpoint for UI events | `@router.websocket("/ws/events")`, keepalive logic (line 44) |
| [`backend/core/job_store.py`](https://github.com/debpalash/VoiceStudio/blob/main/backend/core/job_store.py) | Persistent event storage | `append_event()`, `events_since()` (lines 4-12, 84-86) |

## Summary

- VoiceStudio streams **Server-Sent Events (SSE)** through FastAPI's `StreamingResponse` with `media_type="text/event-stream"` to deliver real-time job updates from the backend to the React frontend.
- The **`job_store`** module in [`backend/core/job_store.py`](https://github.com/debpalash/VoiceStudio/blob/main/backend/core/job_store.py) persists events with sequence numbers, enabling clients to resume interrupted streams using the `after_seq` query parameter without data loss.
- SSE endpoints in **[`backend/api/routers/dub_core.py`](https://github.com/debpalash/VoiceStudio/blob/main/backend/api/routers/dub_core.py)**, **[`backend/api/routers/system.py`](https://github.com/debpalash/VoiceStudio/blob/main/backend/api/routers/system.py)**, and **[`backend/api/routers/setup/download.py`](https://github.com/debpalash/VoiceStudio/blob/main/backend/api/routers/setup/download.py)** implement async generators that yield formatted `data: ...` lines and `:keepalive` comments to maintain HTTP connections.
- The React frontend consumes SSE via the **`EventSource`** API and WebSocket via the **`WebSocket`** API, managing connection lifecycle and JSON parsing through custom hooks that track sequence state.
- This architecture separates job-specific progress streams (SSE) from application-wide notifications (WebSocket), optimizing for both reliability and low-latency UI updates.

## Frequently Asked Questions

### Why does VoiceStudio use both SSE and WebSocket instead of just one protocol?

VoiceStudio leverages **SSE** for **job-specific, unidirectional** data streams where the server pushes progress updates to a listening client, benefiting from SSE's automatic reconnection and HTTP-compatible infrastructure. **WebSocket** handles **bidirectional, application-wide** sidebar notifications where full-duplex communication provides lower latency for UI state changes. This separation allows each transport to optimize for its specific reliability and directionality requirements.

### How does VoiceStudio prevent event loss during network reconnections?

The system assigns **monotonically increasing sequence numbers** to every event stored via `job_store.append_event()`. When the React frontend reconnects using `EventSource`, it includes the last received sequence number as the `after_seq` query parameter. The backend then calls `job_store.events_since()` to emit all missed events before resuming the real-time stream, ensuring exactly-once delivery for persisted events.

### What is the purpose of the `:keepalive` comments in the SSE streams?

These comments follow the Server-Sent Events protocol specification for comment lines (starting with `:`). VoiceStudio emits `:keepalive` periodically—specifically noted in [`backend/api/routers/events.py`](https://github.com/debpalash/VoiceStudio/blob/main/backend/api/routers/events.py)—to prevent intermediate proxies and load balancers from closing idle HTTP connections. Unlike data lines, comment lines do not trigger `onmessage` handlers in the browser, making them ideal for connection maintenance without affecting application logic.

### Can the SSE implementation handle high-throughput scenarios like real-time transcription word-by-word?

Yes, the architecture in **[`backend/api/routers/dub_core.py`](https://github.com/debpalash/VoiceStudio/blob/main/backend/api/routers/dub_core.py)** (lines 592-718) separates event generation from transmission through the persistent `job_store`. The async generator pattern used in the streaming endpoints yields control back to the event loop between transmissions, preventing blocking while maintaining ordered delivery. For high-frequency updates, the system batches events or adjusts the keepalive interval to balance latency against connection overhead.