Designing for Connection Resilience
Real-time applications, especially chat systems, face a fundamental challenge: network instability. Devices sleep, networks change, and browser tabs restore from cache. These events can cause duplicate requests or lost acknowledgments. For critical applications like incident response dashboards or marketplace chat rooms, this means alerts might trigger multiple pages or, worse, go unnoticed. The core problem is trust. A client-side solution alone, relying on simple sets of seen events, cannot guarantee the durability required to survive these disruptions at scale.
The strategy hinges on making event identity durable, implementing deduplication at the consumer boundary, and resuming from a server-issued cursor. This approach ensures that even if a connection drops and reconnects, the system can reliably deliver events without duplicates or loss.
Immutable Event Identity is Paramount
Every event published must possess an immutable identity. This identity must be scoped to the stream of events, not tied to a specific network connection or WebSocket session. For a marketplace chat room, a common pattern is to use a composite identity such as (room_id, sequence_number). The room_id ensures events are correctly segregated by conversation, while the sequence_number provides a strict ordering within that room. This sequence number should be monotonically increasing and assigned by the server upon event publication, acting as a unique, unchangeable identifier for each message or update.
This durable identity is the bedrock of reliable delivery. It allows consumers, regardless of their connection state, to uniquely identify and process each event exactly once. Without this, managing state across reconnections becomes a complex, error-prone task.

Deduplication at the Consumer Boundary
While immutable event identities prevent server-side confusion, the client application must still handle potential duplicates arriving over a re-established connection. The most robust place to implement deduplication is at the consumer boundary – the point where the application logic receives events from the network layer. This means the WebSocket handler or API endpoint receiving data should be responsible for checking if an event with a given identity has already been processed.
A common implementation involves maintaining a short-term memory of recently processed event identities. When a new event arrives, its identity is checked against this memory. If the identity is present, the event is discarded. If it's new, it's processed, and its identity is added to the memory. This memory can be a set or a map, optimized for fast lookups. The size of this memory is a trade-off: larger memory means better protection against duplicates but higher resource consumption. Crucially, this deduplication mechanism must be aware of the server-issued cursor.
Server-Issued Cursors for Resumption
The final piece of the puzzle is enabling the client to resume its state from where it left off after a disconnection. This is achieved through server-issued cursors. When a client connects, it doesn't just establish a new connection; it provides its last known processed event identity or cursor. The server then uses this information to determine the point from which to start sending new events. This ensures that no events are missed during the downtime and that the client receives a clean, ordered stream upon reconnection.
For example, a client might connect and say, "I last processed event (room_id: 123, sequence: 456)." The server would then check its event log or message queue and begin sending events starting from sequence number 457 for room 123. This cursor mechanism is vital. It transforms the system from a stateless broadcast model to a stateful, resilient delivery system. The server effectively acts as the source of truth, dictating the sequence and ensuring continuity.
Node.js Implementation Considerations
Implementing this pattern in Node.js involves careful management of state and asynchronous operations. For durable event identities, a database like PostgreSQL or a distributed key-value store such as Redis can be used to assign and retrieve sequence numbers. The application server would be responsible for generating these unique IDs before publishing events.
For consumer-side deduplication, an in-memory data structure (like a `Set` or `Map` in JavaScript) is suitable for recent event identities. To handle the cursor, the client would store its last acknowledged sequence number persistently (e.g., in `localStorage` or `sessionStorage` for browsers, or device storage for mobile apps). Upon reconnection, this stored cursor is sent to the server. The server's message broker or event bus needs to support querying events based on this cursor. Libraries like Kafka or RabbitMQ, with their consumer group and offset management features, can be adapted for this purpose, although a custom solution might be necessary for highly specific real-time chat requirements.
The challenge lies in ensuring that the client-side deduplication buffer doesn't grow indefinitely and that stale identities are pruned. A time-based expiry or a fixed capacity with a Least Recently Used (LRU) eviction policy can manage this. Furthermore, the server must be able to efficiently retrieve events starting from a specific sequence number, which might require optimized indexing in the event store.
Broader Implications for Real-time Systems
This pattern of durable identity, consumer-side deduplication, and server cursors is not limited to chat applications. It's a foundational principle for any real-time system that requires high availability and guaranteed delivery, such as financial trading platforms, IoT data ingestion pipelines, or collaborative editing tools. By externalizing the responsibility of deduplication to the consumer and ensuring durable event identities managed by the server, developers can build more resilient and scalable real-time experiences that can withstand the inherent unreliability of network connections.
