Live interaction platforms are event storms. A single session between two users generates connection events, presence updates, message events, media state changes, typing indicators, rating events, and session lifecycle events. At scale with hundreds of concurrent sessions, you're processing thousands of events per second. The architecture that handles this isn't about any single technology — it's about how you route, process, and persist events.
Event Classification
We classified events into three categories: (1) Transient events — typing indicators, presence updates, cursor positions. These are broadcast to connected clients but never persisted. They're fire-and-forget via Redis Pub/Sub. (2) State events — session start/end, connection quality changes, media state. These update in-memory state and are persisted for session replay. (3) Durable events — messages, ratings, payments. These are persisted to PostgreSQL via an event store pattern and are the source of truth for the session.
Connection Management at Scale
Each WebSocket connection consumes memory and file descriptors. We implemented connection pooling: a single WebSocket server handles up to 5,000 connections, and we run multiple servers behind a load balancer. Sticky sessions ensure a user's connections always hit the same server (important for in-memory state). When a server approaches capacity, new connections are routed to the next server. Health checks detect dead connections within 30 seconds.
Warning
Always implement heartbeat-based dead connection detection. WebSocket 'close' events are unreliable — many connections die silently without sending them.
Event Sourcing for Session Replay
Every durable event is stored in an append-only event log. This gives us two capabilities: (1) Full session replay — we can reconstruct the exact state of any session at any point in time, useful for dispute resolution and quality assurance. (2) Audit trail — every action is attributed to a specific user with a timestamp, creating accountability. The event store uses PostgreSQL's JSONB columns for flexible event payloads while maintaining query performance via GIN indexes.
Scaling Event Processing
For transient events, we use Redis Pub/Sub with channel partitioning — each session gets its own channel, and servers subscribe only to channels with active users. For durable events, we use a write-behind pattern: events are first acknowledged to the client, then asynchronously persisted to PostgreSQL via a queue. This ensures sub-100ms client acknowledgment while guaranteeing durability. The queue handles backpressure during database maintenance windows.
Conclusion
Event-driven architecture for live platforms is about classification and routing. Transient events go fast and ephemeral (Redis). Durable events go slow and permanent (PostgreSQL). State events bridge the two. Get this classification right, and scaling becomes a matter of adding servers to the appropriate layer rather than redesigning the entire system.
Key Takeaways
- Classify events as transient (Redis Pub/Sub), state (in-memory + persist), or durable (PostgreSQL event store)
- Connection pooling with sticky sessions handles scale without losing in-memory state
- Event sourcing enables full session replay and audit trails for dispute resolution
- Write-behind pattern acknowledges clients fast while persisting durably via queue