# How to Use Socket.IO for Real-Time Events in RomM: Backend Architecture and Client Implementation

> Learn how to use Socket.IO for real-time events in RomM. This guide covers backend architecture and client implementation for seamless event broadcasting from FastAPI workers.

- Repository: [The RomM Project/romm](https://github.com/rommapp/romm)
- Tags: how-to-guide
- Published: 2026-07-05

---

**RomM implements real-time communication using Socket.IO with Redis-backed pub/sub to broadcast scan progress, activity updates, admin logs, and WebRTC net-play signals from FastAPI workers to connected web clients.**

RomM leverages Socket.IO to push live data from the backend to the web UI without requiring polling. The architecture is built around a single `AsyncServer` instance backed by `AsyncRedisManager`, which allows multiple FastAPI workers to share the same socket namespace across a distributed deployment. This implementation supports horizontal scaling while maintaining event consistency for all connected clients.

## Architecture Overview: Redis-Backed Socket.IO Server

The foundation of RomM's real-time system lies in [`backend/handler/socket_handler.py`](https://github.com/rommapp/romm/blob/main/backend/handler/socket_handler.py), which initializes an `AsyncServer` using Redis as the client manager. This design ensures that every emitted event is written to Redis, and all workers subscribe to the same channel, guaranteeing that any client connected to any worker receives identical events.

The server exposes two distinct endpoints:

- **`/ws/socket.io`** – Handles general UI events including scan progress, library activity, and administrative log streams.
- **`/netplay/socket.io`** – Manages peer-to-peer WebRTC signaling for multiplayer gameplay.

## Server Configuration and Initialization

The `SocketHandler` class in [`backend/handler/socket_handler.py`](https://github.com/rommapp/romm/blob/main/backend/handler/socket_handler.py) creates the server instance and mounts it as an ASGI application. The configuration uses `AsyncRedisManager` to enable cross-worker communication and sets specific ping parameters to maintain connection stability.

```python

# backend/handler/socket_handler.py

class SocketHandler:
    def __init__(self, path: str) -> None:
        self.socket_server = socketio.AsyncServer(
            cors_allowed_origins="*",
            async_mode="asgi",
            json=json_module,
            client_manager=socketio.AsyncRedisManager(REDIS_URL),
            ping_timeout=60,
            ping_interval=25,
        )
        self.socket_app = socketio.ASGIApp(
            self.socket_server, 
            socketio_path=path
        )

```

This configuration allows the server to run behind multiple FastAPI workers while maintaining synchronized state through Redis.

## Emitting Real-Time Events

Throughout the codebase, handlers emit events using the `emit()` method of the socket server instance. Events are broadcast either globally or to specific rooms depending on the use case.

### Scan Progress Updates

During library scans, [`backend/endpoints/sockets/scan.py`](https://github.com/rommapp/romm/blob/main/backend/endpoints/sockets/scan.py) emits granular progress updates to the frontend. The handler broadcasts statistics and individual ROM processing status:

```python

# backend/endpoints/sockets/scan.py (lines 98-106, 77-80)

await socket_manager.emit("scan:update_stats", self.to_dict())
await socket_manager.emit("scan:scanning_rom", rom_payload)

```

### Activity Notifications

The activity socket handler in [`backend/endpoints/sockets/activity.py`](https://github.com/rommapp/romm/blob/main/backend/endpoints/sockets/activity.py) sends updates when library changes occur, using the `activity:update` event to notify clients of new or modified content.

### Admin Log Streaming

Administrators receive real-time log entries through the `logs:entry` event. The [`backend/endpoints/sockets/logs.py`](https://github.com/rommapp/romm/blob/main/backend/endpoints/sockets/logs.py) file implements authentication in the `connect` handler, resolving the user from the request and placing authenticated admins into the `"admin"` room:

```python

# backend/endpoints/sockets/logs.py

@socket_server.event
async def connect(sid, environ):
    user = await resolve_user(environ)
    await socket_server.save_session(sid, {"user_id": user.id})
    if user.role == "admin":
        await socket_server.enter_room(sid, "admin")

```

A background task subscribes to the `"romm:logs"` Redis channel and emits lines to the `"admin"` room, ensuring all admin clients receive live logs regardless of which worker they connect to.

## Net-Play Signaling and Room Management

For multiplayer functionality, RomM uses Socket.IO rooms to isolate WebRTC signaling between peers. The [`backend/endpoints/sockets/netplay.py`](https://github.com/rommapp/romm/blob/main/backend/endpoints/sockets/netplay.py) handler manages room lifecycle and session state.

### Creating and Managing Rooms

When a client opens a net-play room, the server creates a room identifier and stores session data in Redis:

```python

# backend/endpoints/sockets/netplay.py (lines 56-58)

await netplay_socket_handler.socket_server.enter_room(sid, session_id)
await netplay_socket_handler.socket_server.save_session(sid, {
    "room_id": room_id,
    "peer_id": peer_id
})

```

### Broadcasting to Specific Peers

To route WebRTC signals between specific users, the server emits to individual socket IDs or rooms while excluding the sender:

```python

# backend/endpoints/sockets/netplay.py (lines 4-9)

await socket_server.emit(
    "webrtc-signal",
    {"sender": sid, "candidate": data["candidate"]},
    to=target_sid,
    skip_sid=sid
)

```

This pattern ensures signaling data reaches only the intended recipient during peer connection establishment.

## Client-Side Implementation

The RomM frontend uses the Socket.IO JavaScript client to establish connections and listen for events. Authentication relies on cookie forwarding via the `withCredentials: true` option.

### Connecting to General UI Events

For scan progress and activity updates, connect to the `/ws/socket.io` endpoint:

```javascript
import { io } from "socket.io-client";

const socket = io("/ws/socket.io", { withCredentials: true });

socket.on("connect", () => console.log("Socket connected"));
socket.on("scan:update_stats", data => {
    // Update progress bar with scan statistics
    console.log("Scan stats:", data);
});
socket.on("scan:scanning_rom", rom => {
    // Display currently scanning ROM
    console.log("Scanning ROM:", rom);
});
socket.on("logs:entry", logEntry => {
    // Append to admin log viewer
    console.log("Log:", logEntry);
});

```

### Connecting to Net-Play Signaling

For WebRTC coordination, use the netplay namespace:

```javascript
const netplay = io("/netplay/socket.io", { withCredentials: true });

// Open a room for multiplayer
netplay.emit("open-room", { gameId: "super_mario_64", roomName: "Player1's Game" });

// Handle incoming WebRTC signals
netplay.on("webrtc-signal", payload => {
    handleSignal(payload);
});

```

The `withCredentials: true` flag is essential as it forwards the authentication cookie, allowing the backend `connect` handlers to resolve the user session and enforce authorization rules.

## Horizontal Scaling with Redis Pub/Sub

Because RomM uses `AsyncRedisManager` as the client manager, the system scales horizontally without additional code. When a worker emits an event via `socket_server.emit()`, the data is written to a Redis pub/sub channel. All other workers subscribe to this channel and broadcast the message to their connected clients.

This architecture is demonstrated in the log forwarding mechanism, where a single worker holds a lock to subscribe to the `"romm:logs"` channel, yet all workers can emit the log entries to their local admin clients. This ensures that even with multiple FastAPI workers behind a load balancer, all clients receive consistent real-time updates.

## Summary

- **Redis-backed architecture**: The `AsyncRedisManager` in [`backend/handler/socket_handler.py`](https://github.com/rommapp/romm/blob/main/backend/handler/socket_handler.py) enables multiple FastAPI workers to share socket state through Redis pub/sub.
- **Dual endpoint design**: Separate namespaces at `/ws/socket.io` (UI events) and `/netplay/socket.io` (WebRTC signaling) isolate concerns.
- **Event emission pattern**: Use `socket_server.emit(event, data)` for global broadcasts or specify `room` and `to` parameters for targeted delivery.
- **Room-based access control**: The `"admin"` room restricts log streaming to authorized users, while net-play rooms isolate peer-to-peer signaling.
- **Client authentication**: The frontend must set `withCredentials: true` to forward cookies for user resolution in `connect` handlers.

## Frequently Asked Questions

### What is the difference between the two Socket.IO endpoints in RomM?

RomM exposes two distinct Socket.IO endpoints to separate concerns. The `/ws/socket.io` endpoint handles general application events including scan progress (`scan:update_stats`), activity updates (`activity:update`), and admin log streams (`logs:entry`). The `/netplay/socket.io` endpoint is dedicated to WebRTC signaling for multiplayer functionality, managing rooms and routing ICE candidates between peers. Both endpoints use the same Redis backend but maintain separate connection namespaces.

### How does RomM handle authentication for Socket.IO connections?

Authentication occurs in the `connect` event handlers, such as in [`backend/endpoints/sockets/logs.py`](https://github.com/rommapp/romm/blob/main/backend/endpoints/sockets/logs.py). The handler inspects the incoming request cookies to resolve the user, then stores the user ID in the socket session using `socket_server.save_session()`. For protected resources like admin logs, the server places the socket into the `"admin"` room only if the user has admin privileges. The frontend must include `withCredentials: true` in the client options to ensure cookies are forwarded during the WebSocket handshake.

### Can RomM's Socket.IO implementation scale across multiple servers?

Yes. By configuring the server with `client_manager=AsyncRedisManager(REDIS_URL)` in [`backend/handler/socket_handler.py`](https://github.com/rommapp/romm/blob/main/backend/handler/socket_handler.py), RomM uses Redis as a message broker between workers. When one worker emits an event, it publishes to Redis, and all other workers receive the message and broadcast it to their local clients. This design allows horizontal scaling across multiple FastAPI processes or servers without requiring sticky sessions or complex coordination code.

### How do I listen for scan progress updates in the frontend?

Connect to the `/ws/socket.io` namespace and register listeners for the specific scan events. The `scan:update_stats` event provides overall progress statistics, while `scan:scanning_rom` fires for each individual ROM being processed. Example implementation:

```javascript
const socket = io("/ws/socket.io", { withCredentials: true });
socket.on("scan:update_stats", stats => updateProgressBar(stats));
socket.on("scan:scanning_rom", rom => updateCurrentRomDisplay(rom));

```

These events are emitted from [`backend/endpoints/sockets/scan.py`](https://github.com/rommapp/romm/blob/main/backend/endpoints/sockets/scan.py) during the library scan process.