Lichess handles real-time communication through a separate WebSocket server called lila-ws, which communicates with the main Scala application (lila) via Redis pub/sub. This architecture enables horizontal scaling and handles millions of concurrent WebSocket connections.
Horizontal scaling: Run multiple lila-ws instances behind load balancerIsolation: WebSocket connections don’t consume resources from main appResilience: Main app restarts don’t drop WebSocket connectionsPerformance: Optimized for connection handling vs. business logicLanguage flexibility: Could rewrite in different language if needed
Complexity: Additional service to deploy and monitorLatency: Extra hop through Redis adds ~1-5msConsistency: Must handle Redis connection failures gracefully
Maintain WebSocket connections for:Game rooms: Players and spectators watching a game
/watch/{gameId} - Spectate game
/play/{gameId} - Play in game
Tournament rooms: Tournament participants and viewers
/tournament/{tournamentId}
Study rooms: Collaborative analysis boards
/study/{studyId}
User notifications: Personal event streams
/user/{userId}
Lobby: Players seeking games
/lobby
Site-wide: Global announcements
/site
Message Routing
lila-ws routes messages between clients and lila:Client → lila:
// Client sends move{"t": "move", "d": {"from": "e2", "to": "e4"}}// lila-ws forwards via Redis to lilaredis.publish("site-in", payload)
lila → Client:
// lila publishes game updateredis.publish("game:abc123", moveData)// lila-ws receives from Redis, sends to clientssocket.send(moveData)
Presence Tracking
lila-ws tracks online users:
// Maintains set of online user IDsprivate val onlineUsers: mutable.Set[UserId] = mutable.Set.empty// Publish online count periodicallyscheduler.scheduleWithFixedDelay(1.second, 1.second): redis.publish("users-online", onlineUsers.size)
final class RemoteSocket( redisClient: RedisClient, lifecycle: play.api.inject.ApplicationLifecycle)(using Executor): // Publish message to specific channel def publish(channel: String, data: JsonData): Unit = redis.publish(channel, Json.stringify(data)) // Send to all clients in a game def sendToGame(gameId: GameId, data: JsonData): Unit = publish(s"game:$gameId", data) // Send to specific user def sendToUser(userId: UserId, data: JsonData): Unit = publish(s"user:$userId", data) // Broadcast to all connected clients def broadcast(data: JsonData): Unit = publish("site", data) // Get set of online user IDs def onlineUserIds: Future[Set[UserId]] = // lila-ws periodically publishes online user count Future.successful(cachedOnlineUsers)