# WebSocket reconnect cookbook

How to build a reliable client against `/v1/ws/odds-fast/{sport_key}`.
Pairs with the reference clients at:

* `examples/ws_reference_client.py` (Python)
* `examples/ws_reference_client.js` (Node and browser)

Most WebSocket support tickets come from clients that don't handle
three things: silent watchdogs, the difference between connection
death and market quiet, and reconnect backoff. This cookbook covers
each.

## The connection lifecycle

1. **Connect** to `wss://parlay-api.com/v1/ws/odds-fast/{sport_key}?apiKey=YOUR_KEY`.
2. **Send messages** to control the session (subscribe, ping).
3. **Receive frames** of type `odds_update`, `heartbeat`, `pong`, `subscribed`.
4. **Detect silence** with a watchdog and force a reconnect when needed.
5. **Reconnect** with capped exponential backoff.

## Frame shapes

### `odds_update` (push-driven, no fixed cadence)

```json
{
  "type": "odds_update",
  "event_id": "evt_abc",
  "sport_key": "baseball_mlb",
  "home_team": "Yankees",
  "away_team": "Red Sox",
  "market": "h2h",
  "side": "home",
  "bookmaker": "draftkings",
  "price": -135,
  "implied_prob": 0.5745,
  "timestamp_ms": 1779100000000
}
```

Emitted whenever a price changes upstream. Quiet markets produce
zero of these for stretches.

### `heartbeat` (every 5 seconds, always)

```json
{
  "type": "heartbeat",
  "timestamp": 1779100000,
  "connections": 23,
  "quiet_seconds": 12.3,
  "upstream": {
    "worst_sla": "ok",
    "counts": {"ok": 6, "degraded": 9, "breach": 1, "stale": 0, "missing": 0},
    "generated_at_ms": 1779099995000
  },
  "bookmaker_freshness": [
    {
      "bookmaker": "pinnacle",
      "polled_at_ms": 1779099998200,
      "poll_age_s": 1.8,
      "status": "observed"
    }
  ]
}
```

Two fields to act on:

* **`quiet_seconds`**: time since the last `odds_update` to your
  socket. Long quiets are normal during off-peak hours.
* **`upstream.worst_sla`**: worst SLA classification across all books
  in the last cycle. If this is `"ok"` and your socket is quiet,
  the market is just slow. If this is `"stale"` or `"breach"`, an
  upstream is having a moment and reconnecting won't help.
* **`bookmaker_freshness`**: for connections with an explicit
  `bookmakers=` filter, the latest successful source poll for each requested
  book and this sport. `status="missing"` and null timestamps mean no current
  pulse is available. This is source/sport reachability, not proof that every
  retained quote changed or was individually reverified; keep each row's
  `last_update` as the price-age signal.

### `pong` (response to client `ping`)

```json
{
  "type": "pong",
  "timestamp": 1779100000,
  "quiet_seconds": 12.3
}
```

Use this for application-level health when the 5-second heartbeat
is too coarse for your loop.

## The three causes of silence (and what to do)

| Symptom | Cause | Action |
|---------|-------|--------|
| No frames AT ALL for 30s+ | Connection died | Force reconnect |
| `heartbeat`s arriving, `quiet_seconds` high, `upstream.worst_sla = "ok"` | Market is quiet | Wait |
| `heartbeat`s arriving, `quiet_seconds` high, `upstream.worst_sla in {"stale","breach"}` | Upstream degraded | Wait or fall back to a different sport_key |

The middle row is the one that historically caused customer-side bugs.
Clients that reconnected on every long quiet ended up in a thundering
herd against our gateway, which was harmless on our end but pointless
work on theirs.

## Silent watchdog (the critical pattern)

```python
last_msg_ts = time.time()
SILENT_WATCHDOG_S = 30  # heartbeat fires every 5s; 30s of silence is real

async def watchdog():
    while True:
        await asyncio.sleep(5)
        if time.time() - last_msg_ts > SILENT_WATCHDOG_S:
            await ws.close(code=1000, reason="silent watchdog")
            return

# In your receive loop:
async for raw in ws:
    last_msg_ts = time.time()
    ...
```

Why 30 seconds: our heartbeat fires every 5s. Anything past 4-5
missed heartbeats is a real connection death, not transient network
jitter.

## Reconnect with capped exponential backoff

```python
backoff_s = 1.0
while True:
    try:
        await run_one_session()
        backoff_s = 1.0
    except ConnectionClosed:
        await asyncio.sleep(backoff_s)
        backoff_s = min(60.0, backoff_s * 2)
```

Cap at 60s. Never reconnect faster than 1s. Don't use unbounded
exponential growth (you'll wait hours after a long outage).

## Subscribe to a specific event

Most clients want every odds_update for the sport_key. If you only
care about one game:

```json
{"type": "subscribe", "event_id": "evt_abc"}
```

The server filters subsequent broadcasts to your socket. Send
`{"type":"unsubscribe"}` to clear the filter.

## Common anti-patterns

**Reconnect on every quiet period.** Long quiets are not connection
deaths. The heartbeat is your liveness signal; trust it.

**Ignore the heartbeat.** Some clients listen only for `odds_update`
and treat all other frames as noise. The heartbeat carries actionable
health data; parse it.

**Don't cap reconnect backoff.** Unbounded exponential growth means
a single 30-minute outage gives you a 30-minute reconnect delay
forever after, even when service is restored.

**Block the receive loop with synchronous work.** Your price storage
/ trade decision / alert pipeline should be async or run on a
separate task. If you block the WebSocket receive coroutine, you
fall behind and the server eventually disconnects you for being slow.

**Trust protocol-level WebSocket pings.** They're fine for the
transport but they don't tell you anything about our pipeline. Use
the application-level `ping`/`pong` if you need granular health.

## Where to look when something looks wrong

* `https://parlay-api.com/v1/status` for a JSON snapshot of all-source
  health right now.
* `https://parlay-api.com/v1/meta/source-quality` for per-source
  detail including SLA classifications.
* `https://parlay-api.com/changelog` for parser-level deploy notes
  including any recently-disclosed degradation.
* `https://parlay-api.com/v1/meta/per-book-sla` for the thresholds
  we use per book.

If after checking those three the behavior still looks wrong, post in
r/parlayapi or email support@parlay-api.com. Reference clients above
are the recommended starting point.
