# WebSocket best practices

A production consumer in five habits.

## 1. Reconnect with backoff

Connections drop: deploys (`1001`), network changes, laptop sleep. Reconnect automatically with exponential backoff and jitter (1s, 2s, 4s … capped at 30s), and re-send your subscriptions after every reconnect.

## 2. Detect dead connections

The server sends a `heartbeat` every 30 seconds. If you see no message for 45 seconds, assume the connection is dead, close it and reconnect.

## 3. Backfill gaps over REST

Keep the newest `published_at` you've processed. After reconnecting, call `GET /news` with the same filters and `from=` that timestamp, then de-duplicate by `id`.

## 4. Read fast, process elsewhere

Don't do slow work (HTTP calls, database writes, model inference) inside the read loop. Push messages onto an in-memory queue and process them in a worker. Slow readers get disconnected with `4030`.

## 5. One connection per process

A single connection can carry 200 channels. Use the 3 connections on the Pro plan for redundancy or separate environments, not one per ticker.

## Reference implementation

resilient consumer

```python
import asyncio
import json
import os
import random
import time

import httpx
import websockets

KEY = os.environ["MARKETNEWSAPI_KEY"]
CHANNELS = ["ticker:AAPL", "ticker:NVDA", "type:press_release"]
last_seen = None
seen_ids = set()

async def handle(article):
 if article["id"] in seen_ids:
 return
 seen_ids.add(article["id"])
 print(article["published_at"], article["title"])

async def backfill(since):
 async with httpx.AsyncClient(timeout=10) as http:
 r = await http.get(
 "https://api.marketnewsapi.com/v1/news",
 params={"tickers": "AAPL,NVDA", "from": since, "sort": "published_asc", "limit": 100},
 headers={"Authorization": f"Bearer {KEY}"},
 )
 for a in r.json()["data"]:
 await handle(a)

async def run():
 global last_seen
 delay = 1
 while True:
 try:
 async with websockets.connect(f"wss://ws.marketnewsapi.com/v1/stream?api_key={KEY}") as ws:
 await ws.send(json.dumps({"action": "subscribe", "channels": CHANNELS}))
 if last_seen:
 await backfill(last_seen)
 delay = 1
 while True:
 msg = json.loads(await asyncio.wait_for(ws.recv(), timeout=45))
 if msg["type"] == "article":
 last_seen = msg["data"]["published_at"]
 await handle(msg["data"])
 except websockets.ConnectionClosed as e:
 if e.code in (4001, 4003):
 raise
 except (asyncio.TimeoutError, OSError):
 pass
 await asyncio.sleep(delay + random.random())
 delay = min(delay * 2, 30)

asyncio.run(run())
```

> Comma-separated tickers are OR'd, so one backfill call covers every symbol on the subscription.
