Recover from disconnects
Sequence numbers, gap detection, and why there is no replay buffer.
Every server→client frame carries seq, a per-(connection, match) counter. A snapshot is seq
0; each following update increments.
Detect a gap
const lastSeq = new Map()
function onFrame(frame) {
if (frame.type === 'snapshot') {
lastSeq.set(frame.match_id, 0)
replaceState(frame.match_id, frame.data)
return
}
if (frame.type !== 'update') return
const expected = (lastSeq.get(frame.match_id) ?? 0) + 1
if (frame.seq !== expected) {
// We missed something. Re-subscribing re-baselines with a fresh snapshot.
ws.send(JSON.stringify({ type: 'subscribe', data: { match_ids: [frame.match_id] } }))
return
}
lastSeq.set(frame.match_id, frame.seq)
applyUpdate(frame.match_id, frame.data)
}The recovery for a gap is always the same: re-subscribe. Every subscribe begins with a fresh
snapshot, so your state is rebuilt from scratch rather than patched.
There is no replay buffer
You cannot resume from your last seq, and you don't need to. The snapshot makes a reconnect
correct, not merely tolerable — which is a deliberate trade: a replay buffer would add server
state, a retention window and a new class of "how far back can I go" bugs, to solve a problem a
snapshot already solves.
Reconnect with backoff
let attempt = 0
async function connect() {
const { data } = await mintTicket() // POST /v1/cs2/ws-token — one metered request
const ws = new WebSocket(`wss://api.esportsodds.gg/v1/ws?token=${data.token}`)
ws.addEventListener('open', () => {
attempt = 0
ws.send(JSON.stringify({ type: 'subscribe', data: { match_ids: [...watched] } }))
})
ws.addEventListener('close', () => {
const wait = Math.min(30_000, 2 ** attempt * 500) + Math.random() * 500
attempt += 1
setTimeout(connect, wait)
})
}Cap the backoff (30s here) and add jitter, so a fleet of clients that dropped together doesn't reconnect in lockstep.
Back off harder on connection_limit
If the close was preceded by an error frame with code: "connection_limit", you are at your
five-connection cap. Reconnecting immediately just burns ticket mints against your quota. Wait,
and check whether an old connection is failing to close.
Unsolicited snapshots are normal
When our internal feed reconnects, every live subscription gets a fresh snapshot you didn't ask
for. It is not an error and not a duplicate to suppress — handle snapshot as "replace this match's
state" and it costs you nothing.
Ticket reuse
A ticket is valid for 60 seconds and is not single-use. A reconnect inside that window can reuse the one you have; only mint again once it's expired. Each mint is a metered request, so a tight reconnect loop that mints every time is a real cost.