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
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.