BlogPrediction markets

Kalshi WebSocket API Clients That Survive Disconnects

The Kalshi WebSocket API is the streaming side of Kalshi's developer access: instead of asking for prices over and over, your program holds a connection open and Kalshi sends changes as they happen. Kalshi documents its channels, message formats, authentication and limits in its own API docs, and those details change, so take them from there.

What doesn't change is the hard part. Any streaming client has to cope with dropped connections, missed messages and state that drifts out of date without telling you. This guide shows a client shape that handles those, in Python, with the venue-specific parts left as clearly marked placeholders.

When to stream instead of poll

Polling is simpler and good enough for many jobs. A stream earns its complexity when:

  • you keep an order book and need every change, not a sample of them;
  • you react to price moves, and a few seconds of polling delay costs you;
  • you'd otherwise make so many requests that you'd hit rate limits.

If you only need a daily snapshot of markets for research, poll. If you need to know the book right now, stream. Searches for "kalshi api websocket" almost always come from the second group.

What Kalshi's docs will tell you

Before writing code, find these in Kalshi's documentation:

  1. The connection address, and whether it differs between any test and live environments.
  2. How the connection authenticates. Streaming connections often authenticate differently from plain requests, and your key handling has to match. Our guide to handling a Kalshi API key covers keeping that secret out of code and logs.
  3. The channels you subscribe to, and the subscribe message format.
  4. Whether messages carry a sequence number, and what the server sends first: a full snapshot, or only changes.
  5. Heartbeats or pings, and how long a quiet connection lasts before it's closed.
  6. Limits on connections and subscriptions.

Items 4 and 5 decide how your client detects trouble, so read them carefully.

A reconnecting client in Python

This uses the websockets library and asyncio. The address, the auth headers and the subscribe message are placeholders: fill them from Kalshi's docs.

import asyncio, json, random
import websockets

async def stream(url, headers, subscribe, on_message, max_delay=60.0):
    delay = 1.0
    while True:
        try:
            async with websockets.connect(url, additional_headers=headers) as ws:
                await ws.send(json.dumps(subscribe))
                first = True
                async for raw in ws:
                    if first:
                        delay, first = 1.0, False  # reset only after real data
                    on_message(json.loads(raw))
        except (websockets.ConnectionClosed, OSError) as exc:
            print(f"disconnected: {exc!r}")
        wait = delay + random.uniform(0, delay / 2)
        await asyncio.sleep(wait)
        delay = min(delay * 2, max_delay)

The choices in it, and why:

  • Exponential backoff with jitter. Waits of about 1, 2, 4, 8 seconds and so on, capped at 60, with a random extra. Without the jitter, every client that dropped at once reconnects at once.
  • Reset only after a message arrives. A server that accepts and then closes straight away would otherwise get a reconnect every second forever.
  • Subscribe on every connect. A new connection starts with no subscriptions.
  • The keyword for headers differs between versions of the websockets library, so check the one you've pinned.

Detecting gaps and rebuilding state

A reconnect is not the only way to lose data. If the stream numbers its messages, compare each one with the last:

class Tracker:
    def __init__(self, resync):
        self.last_seq = None
        self.resync = resync  # callback that fetches a fresh snapshot

    def check(self, seq: int) -> bool:
        if self.last_seq is not None and seq != self.last_seq + 1:
            self.last_seq = None
            self.resync()
            return False  # drop this message; state is being rebuilt
        self.last_seq = seq
        return True

On a gap, throw away what you hold and load a fresh snapshot. Patching around a missing message leaves you with a book that looks fine and is wrong. The same rule applies after every reconnect: start from a snapshot, then apply changes. If Kalshi websockets don't send a snapshot on subscribe, take one over REST before applying the first change, and drop changes older than it.

Keeping time honest

Store two times for every message: the time Kalshi stamps on it, if it has one, and the time you received it. Kalshi websockets that go quiet don't mean prices stopped moving. If your heartbeat check fails, mark everything you hold as stale until a snapshot arrives, and never show an old price as current.

How ZippyOddsAPI's own stream does it

ZippyOddsAPI is independent and not affiliated with Kalshi. Its own WebSocket stream at wss://zippyoddsapi.com/v1/stream follows the same pattern, which is a fair check on the design above:

  • Messages that change state carry a seq. On a gap, the client sends {"type": "resync"} and gets a fresh snapshot.
  • A heartbeat about every 15 seconds repeats the latest seq, so a client can tell it missed something even when nothing else arrives.
  • A large snapshot arrives in parts, each with part and parts.
  • From a server, the key goes in a header. A key in the URL is refused.

That stream carries sportsbook, exchange and pick'em prices, and it needs a plan with the stream feature. ZippyOddsAPI also keeps a separate REST endpoint, /v1/prediction-contracts, for raw prediction-market observations, and its documented source is Polymarket US. The stream and endpoint docs give the details.

If you'd rather not write the socket handling yourself, see whether a library already does it in our guide to setting up the Kalshi Python SDK. For the wider picture across venues, read the prediction market API overview.