All questions

WebSocket Session Resume

Premium

WebSocket Session Resume

WebSocket session resume is how a live connection recovers from a drop without losing or duplicating a single message: the client remembers the last message it processed, and on reconnect it asks the server to replay everything after that point. It is the protocol behind the Discord and Slack gateway resume, MQTT session continuity, and Kafka-style consumer offsets. This question is about the resume itself — assume the socket already dropped and reconnected; your job is to make the message stream continuous across that gap.

Every server message is a data frame { seq, data }, where seq is a counter that increases by one for each message. You track lastSeq, the highest contiguous seq you have handed to the app. The transport is injected — you never open a real socket — so this is pure bookkeeping over sequence numbers.

Signature

function websocketSessionResume(options: {
  send: (frame: object) => void;      // write a control frame (the resume request) to the server
  onMessage: (data: unknown) => void; // sink for DE-DUPED, in-order messages to the app
  sessionId?: string;                 // optional id echoed in the resume frame
  startSeq?: number;                  // optional starting high-water mark (default 0)
}): {
  receive: (frame: object) => void;   // the transport calls this with every inbound frame
  resume: () => void;                 // after reconnect: send { type: 'resume', lastSeq }
  readonly lastSeq: number;           // highest contiguous seq delivered
};

Examples

const delivered = [];
const sent = [];
const s = websocketSessionResume({
  onMessage: (d) => delivered.push(d),
  send: (f) => sent.push(f),
});

s.receive({ seq: 1, data: 'a' });
s.receive({ seq: 2, data: 'b' });
s.receive({ seq: 2, data: 'b' }); // a redelivered duplicate — dropped
// delivered === ['a', 'b'],  s.lastSeq === 2
// The socket dropped after seq 5, then reconnected. Ask for a replay:
s.resume();
// sent === [{ type: 'resume', lastSeq: 5 }]

// The server replays 4..8 (at-least-once, so 4 and 5 come again):
[4, 5, 6, 7, 8].forEach((seq) => s.receive({ seq, data: 'm' + seq }));
// 4 and 5 are dropped as duplicates; only 6, 7, 8 are delivered — s.lastSeq === 8

Notes

  • Injected transport — you get send and onMessage in options and never touch a real WebSocket. This is what makes the protocol testable by hand.
  • Contiguous onlylastSeq advances only when the next expected seq arrives. A frame whose seq is lastSeq + 1 is delivered; anything past that leaves a gap.
  • De-duplicate — a frame whose seq is lastSeq or lower has already been processed. Drop it; never deliver it twice. This is what turns at-least-once delivery into exactly-once processing.
  • Buffer gaps — a frame whose seq is more than one past lastSeq means an earlier message is still missing. Hold it, and flush it in order once the hole is filled.
  • Resume frameresume() sends { type: 'resume', lastSeq } so the server knows exactly where to replay from. Replayed frames arrive as ordinary data frames.
  • Out of scope — you do not implement reconnection, backoff, or the server. Assume the reconnect already happened; focus on the sequence logic.

FAQ

Why does the client de-duplicate if the server already knows the last seq it sent?
Real transports deliver at-least-once: a frame the server thinks it already sent can arrive twice after a reconnect, or the resume replay can overlap messages the client already processed. Tracking lastSeq and dropping any seq at or below it turns at-least-once delivery into exactly-once processing, so a redelivered message is never applied twice.
What is the difference between a duplicate and a gap?
Both are judged against lastSeq, the highest contiguous seq already delivered. A frame whose seq is at or below lastSeq is a duplicate and is dropped. A frame whose seq is more than one past lastSeq means an earlier frame is still missing, so it is a gap: the frame is buffered and delivered only once the hole is filled.
Why inject the transport instead of opening a real WebSocket?
The resume protocol is pure bookkeeping over sequence numbers, so it needs no real socket. Passing in a send function and an onMessage sink lets a test feed inbound frames by hand with receive(), assert the exact resume frame the client sent, and collect delivered messages into an array — deterministically, with no network and no timers.

Unlock the solution & editor

  • Runnable editor + tests

    Solve in the browser with instant Jest feedback.

  • Detailed solutions

    Walkthroughs, edge cases, and complexity notes.

  • Multi-framework variants

    React, Vue, Vanilla, Angular — same question, different stacks.

Upgrade to Premium