How-to

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.

On this page