Skip to content

Kalshi reference data is re-emitted in a loop, saturating the WebSocket broadcast path #149

Description

@juan-malbeclabs

The problem

instrument frames for Kalshi are re-emitted continuously at tens of thousands per second,
far beyond any rate the underlying product set can justify. The broadcast serializer cannot
keep up, drops everything queued behind the flood, and what survives to a subscriber is
almost exclusively book clear messages with their following levels lost.

The emission rate is not a function of how much data there is. It is a function of how many
channels are enabled — which is what identifies it as a loop rather than load.

Evidence that it is a loop

Twenty-second samples of instrument frames off ws://127.0.0.1:8081, subscribed to
{"method":"subscribe","subscription":{"type":"instrument"}}, counted by
(source_name, channel). Same host, same bridge process, minutes apart, only the channel
filter changed:

all 31 sports channels admitted only edge-kalshi-sports-mbp=11 admitted
KALSHI:11 15,357/s 70,604/s
all channels 38,598/s 94,661/s

The same channel emits 4.6x faster when it is the only one admitted, and the total goes
up rather than down.
Restricting the channel set does not reduce the work; it makes the
producer cycle faster over the channels that remain.

Top channels with everything admitted (20s, 771,952 frames total, top 15 are all Kalshi):

KALSHI:11  307,158     KALSHI:10   95,214     KALSHI:50   55,225
KALSHI:39   52,120     KALSHI:17   49,376     KALSHI:49   28,874
KALSHI:55   13,870     KALSHI:52   13,771     KALSHI:15   13,227

Of that total, roughly 77% is sports and at least 12% is elections. No non-Kalshi channel
reached the top 15, so Hyperliquid and Phoenix were each emitting under 400/s — two orders of
magnitude below KALSHI:11 on the same connection.

An earlier measurement on the same host put KALSHI:10 at ~132,000/s while /v1/status
reported 22 products on that channel, and recorded 703 drop events totalling 3,362,845
frames over three minutes.

What it breaks downstream

Measured from a consumer holding a single WebSocket connection:

framesPerSec : 144,488
instrument   : 887,725      99.86% of all frames
depth        :     630
book         :     509
order_book   :      68
rebaselines  :     577

book + order_book = 577 and rebaselines = 577. Every book message that arrives is a
re-baseline
— a clear whose following levels never made it. Confirmed again over a
separate 30-second window: 5,697 book messages, 5,697 rebaselines.

The consequences follow directly:

  • Books never complete. A market either reports live holding no levels, or sits waiting
    for a venue snapshot indefinitely, because the batch that would baseline it always loses
    frames before its terminating message.
  • No trade frames survive at all, so no candles are built from them, while the HTTP
    history endpoint keeps returning current buckets for the same products. The data exists;
    only the broadcast path is losing it.
  • /v1/status has been observed timing out at 30s under the same load.

Why the channel filter is not a workaround

POST /admin/channels?channels=edge-kalshi-sports-mbp=11 applies correctly and cleanly
(feeds not named keep their registry defaults, channels= restores the prior state), but as
the table above shows it does not reduce the flood — it concentrates it. Excluding the
single busiest channel cut observed traffic roughly in half while leaving rebaselines
still exactly equal to the book message count: the pathology was unchanged.

Kalshi elections floods on its own, so a deployment that only wants elections cannot filter
its way out either.

Environment

  • Bridge: ghcr.io/malbeclabs/doublezero-edge-connect:mainnet-beta, on an AWS host with the
    consumer on the same box
  • Built-in feed registry (DZ_FEED_REGISTRY_URL empty), no channel filter applied
  • Measured 2026-09-05

How to reproduce the measurement

Connect a WebSocket client to the bridge, subscribe to
{"method":"subscribe","subscription":{"type":"instrument"}}, and count frames by
(source_name, channel) over 20 seconds. Count by scanning the raw frame bytes rather than
parsing JSON — at these rates a JSON parser makes the client the bottleneck, and a lagging
client is handed the replay that caused the lag, which contaminates the sample.

Then apply ?channels=edge-kalshi-sports-mbp=11, repeat, and compare the per-channel rate
for channel 11 between the two runs.

What we are asking for

The re-emission itself, in the publisher or in the bridge's decode of it. A rate this high
against a product set this small is duplication, and no amount of consumer-side or host-side
tuning addresses it: the hot path is a single serializer thread and the traffic is redundant
rather than heavy, so CPU headroom on the host buys nothing.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

No labels
No labels

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions