# How RomM Implements Netplay Using WebSockets: Socket.IO and Redis Architecture

> Discover how RomM implements netplay with WebSockets. Explore its Socket.IO and Redis architecture built on FastAPI for seamless real-time player synchronization and room state management.

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

---

**RomM implements netplay by layering a Socket.IO server on top of FastAPI and using Redis to manage transient room state and real-time player synchronization.**

RomM is an open-source game library manager that enables multiplayer gaming through browser-based emulation. The platform uses **WebSockets** to facilitate real-time communication between players, creating a seamless netplay experience without requiring dedicated game servers. This implementation relies on a combination of Socket.IO for bidirectional event handling and Redis for distributed state management.

## WebSocket Server Architecture

The netplay system centers on a **Socket.IO ASGI application** mounted within RomM's FastAPI backend. In [`backend/main.py`](https://github.com/rommapp/romm/blob/main/backend/main.py), the application mounts the Socket.IO server at the `/netplay` endpoint, creating a dedicated WebSocket entry point for multiplayer sessions.

The `SocketHandler` class in [`backend/handler/socket_handler.py`](https://github.com/rommapp/romm/blob/main/backend/handler/socket_handler.py) initializes an `AsyncServer` configured with a Redis-backed client manager:

```python

# backend/handler/socket_handler.py

socketio.AsyncServer(
    client_manager=socketio.AsyncRedisManager(REDIS_URL),
    cors_allowed_origins="*",
    async_mode="asgi"
)

```

This Redis integration ensures that all Socket.IO nodes share the same pub-sub channel, enabling horizontal scaling across multiple backend processes. When clients connect to `/netplay/socket.io`, they enter a distributed WebSocket infrastructure capable of handling concurrent multiplayer rooms across server instances.

## Room State Management with Redis

Netplay rooms are ephemeral data structures stored in a Redis hash named `netplay:rooms`. The `NetplayHandler` class in [`backend/handler/netplay_handler.py`](https://github.com/rommapp/romm/blob/main/backend/handler/netplay_handler.py) implements the persistence layer with CRUD operations including `get()`, `set()`, `delete()`, and `get_all()` methods that JSON-serialize room objects.

Each room contains:
- **Owner**: The socket ID of the room creator
- **Players**: A dictionary mapping player IDs to connection metadata
- **Peers**: WebRTC peer connection data
- **Configuration**: Room name, game ID, password protection, and maximum player limits

When a player creates a room, the handler validates the session ID and stores the serialized `NetplayRoom` object in Redis, making the room immediately available to other clients querying the `netplay:rooms` hash.

## Netplay Lifecycle Events

The Socket.IO server in [`backend/endpoints/sockets/netplay.py`](https://github.com/rommapp/romm/blob/main/backend/endpoints/sockets/netplay.py) registers handlers for the complete netplay lifecycle, managing room creation, player joins, and session cleanup.

### Creating Rooms

The `open-room` event handler validates incoming data and initializes new game sessions:

```python
@netplay_socket_handler.socket_server.on("open-room")
async def open_room(sid: str, data: RoomData):
    extra = data["extra"]
    session_id = extra.get("sessionid")
    player_id = extra.get("userid") or extra.get("playerId")
    
    if not session_id or not player_id:
        return "Invalid data: sessionId and playerId required"
    
    if await netplay_handler.get(session_id):
        return "Room already exists"
    
    new_room = NetplayRoom(
        owner=sid,
        players={player_id: NetplayPlayerInfo(
            socketId=sid,
            player_name=extra.get("player_name") or f"Player {player_id}",
            userid=extra.get("userid"),
            playerId=extra.get("playerId"),
        )},
        room_name=extra.get("room_name") or f"Room {session_id}",
        game_id=extra.get("game_id") or "default",
        password=extra.get("room_password"),
        max_players=data.get("maxPlayers") or DEFAULT_MAX_PLAYERS,
    )
    
    await netplay_handler.set(session_id, new_room)
    await netplay_socket_handler.socket_server.enter_room(sid, session_id)
    await netplay_socket_handler.socket_server.save_session(
        sid, {"session_id": session_id, "player_id": player_id}
    )
    await netplay_socket_handler.socket_server.emit(
        "users-updated", new_room["players"], room=session_id
    )

```

### Joining and Leaving Rooms

The `join-room` and `leave-room` handlers manage player state transitions using `socket_server.enter_room()` and `socket_server.leave_room()` methods. These calls update the Redis-backed room membership and trigger `users-updated` events to synchronize the player list across all connected clients.

Session persistence is maintained through `socket_server.save_session()`, which stores `session_id` and `player_id` metadata for each socket connection, enabling automatic reconnection and cleanup handling.

## WebRTC Signaling for Peer-to-Peer Communication

RomM leverages **WebRTC** to enable peer-to-peer video and audio streaming between players without routing media through the server. The system implements signaling handlers that forward connection metadata between clients:

```python
@netplay_socket_handler.socket_server.on("webrtc-signal")
async def webrtc_signal(sid: str, data: WebRTCSignalData):
    target = data.get("target")
    if not target:
        return
    
    await netplay_socket_handler.socket_server.emit(
        "webrtc-signal",
        {
            "sender": sid,
            "candidate": data.get("candidate"),
            "offer": data.get("offer"),
            "answer": data.get("answer"),
        },
        to=target,
    )

```

The `webrtc-signal` and `webrtc-signal-error` events handle ICE candidates, session offers, and renegotiation requests by re-emitting payloads to target socket IDs. This architecture minimizes server load while enabling low-latency communication between emulator instances.

## Automatic Room Cleanup

To prevent stale state accumulation, RomM implements a scheduled background task in [`backend/tasks/scheduled/cleanup_netplay.py`](https://github.com/rommapp/romm/blob/main/backend/tasks/scheduled/cleanup_netplay.py). This task periodically scans the `netplay:rooms` Redis hash to identify and remove empty rooms (those with no active players), ensuring efficient memory usage and preventing orphaned game sessions from consuming resources indefinitely.

## Frontend WebSocket Integration

The Vue.js frontend consumes the netplay API through Socket.IO clients initialized in [`frontend/src/views/Player/EmulatorJS/Player.vue`](https://github.com/rommapp/romm/blob/main/frontend/src/views/Player/EmulatorJS/Player.vue). When users initiate netplay, the frontend injects configuration parameters including `EJS_netplayServer` and `EJS_netplayICEServers`, then establishes connections to `/netplay/socket.io`.

Client-side room creation follows this pattern:

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

const socket = io(`${window.location.origin}/netplay/socket.io`);

socket.emit("open-room", {
  extra: {
    sessionid: "abc123",
    userid: "user42",
    player_name: "Alice",
    room_name: "My Netplay Room",
    game_id: "snes-mario",
  },
  maxPlayers: 4,
});

socket.on("users-updated", (players) => {
  console.log("Current players:", players);
});

```

The frontend listens for `users-updated` events to refresh player lists and processes `webrtc-signal` events to establish peer connections between browser-based emulators.

## Summary

- **RomM implements netplay using WebSockets** through a Socket.IO server mounted on FastAPI at `/netplay`, backed by Redis for distributed state management.
- **Room state persists in Redis** using the `netplay:rooms` hash, with `NetplayHandler` providing CRUD operations for ephemeral multiplayer sessions.
- **Socket.IO events** manage the complete lifecycle: `open-room`, `join-room`, `leave-room`, and `disconnect`, using `enter_room()` and `save_session()` for connection management.
- **WebRTC signaling** is forwarded peer-to-peer through the `webrtc-signal` event handler, enabling direct browser-to-browser communication without media server overhead.
- **Automatic cleanup** prevents resource leaks by scanning for and deleting empty rooms via scheduled tasks in [`cleanup_netplay.py`](https://github.com/rommapp/romm/blob/main/cleanup_netplay.py).

## Frequently Asked Questions

### How does RomM handle multiple server instances with WebSockets?

RomM uses **Redis as a message broker** through Socket.IO's `AsyncRedisManager`. This configuration stores room state and pub-sub channels in Redis, allowing multiple FastAPI worker processes to share socket state and broadcast events across the entire server cluster.

### What data is stored in a RomM netplay room?

Each room contains the owner socket ID, a dictionary of connected players with their socket IDs and metadata, WebRTC peer information, room configuration (name, game ID, password), and maximum player limits. This data is JSON-serialized and stored in the Redis `netplay:rooms` hash.

### How does RomM clean up abandoned netplay rooms?

A scheduled background task defined in [`backend/tasks/scheduled/cleanup_netplay.py`](https://github.com/rommapp/romm/blob/main/backend/tasks/scheduled/cleanup_netplay.py) periodically scans the Redis room storage to identify rooms with no active players. The task automatically deletes these empty entries, preventing memory leaks and keeping the room list current.

### Can RomM's netplay system scale horizontally?

Yes. By using Redis as the client manager for Socket.IO, RomM's netplay implementation supports horizontal scaling. Multiple backend instances can handle WebSocket connections simultaneously while sharing room state through the centralized Redis store, ensuring consistent player synchronization across the server fleet.