Order-book collector

It subscribes to every live market on a prediction-market exchange, keeps an L2 order book for each one, and writes every price-level change to stdout, plus a snapshot of every book once a minute, in the challenge's aligned SBE format. Every file it writes can be replayed to prove the books are exact.

The result

Two hours on AWS, both venues at once: Polymarket's full universe from London (c7i.xlarge, 4 vCPU, 8 GB) and Kalshi's whole demo universe from Virginia. The hour-long run the day before held 550k Polymarket books at 42k updates/s.

690korder books held: 488k Polymarket, 200k Kalshi, on about 560 websockets
264Mprice-level updates written in the two hours
0mismatches in 167M snapshots replayed
5–6 msp50, exchange event to collector, London
2.0 GBpeak memory for the full Polymarket universe; load 1.1 of 4 CPUs

The pipeline

One engine, five stages. Only the first two know which venue they are talking to; that seam is the ExchangeAdapter trait (Ext 3).

  1. 01DiscoveryLists every live market at start and every 10 minutes, and follows the venue's listing and resolution events in between. New markets join; resolved ones are dropped.
  2. 02Shards and socketsMarkets are split into shards of 2,500 tokens. Each shard gets two websockets on different edge servers (Ext 1), so losing one socket or one edge loses nothing.
  3. 03DecodeTokio workers read the sockets and turn the venue's JSON into a common event: a price, a size and a timestamp.
  4. 04Core threadsEach core thread owns whole markets. It applies events to flat sorted books, chooses which replica is the source, and checks each book against the venue's hash.
  5. 05WriterEncodes updates and snapshots with the template's SBE helpers into 64 MiB chunks. Each chunk is written as it fills, then the file is rotated off the hot path.

When something goes wrong

Each case below has a test that injects the fault against a mock venue, and most were also seen live.

A socket drops
Its replica is already the source or becomes it. A snapshot is written at the switch, so the output stays exact. The socket reconnects with backoff.
A socket goes silent
Replaced once a peer has been ahead for 3 s (--lag-ms). Quiet sockets are reconnected; every socket is recycled hourly, make-before-break (Ext 2).
A token goes quiet
Per-token rate checks resubscribe only tokens that are quiet for their own rate, with a wait that doubles on each strike (Ext 2).
A book is corrupted
The venue's book hash disagrees; the book is resynced from a fresh snapshot.
The process dies
--supervise restarts it in about a second and keeps every whole chunk of the open file.
Ctrl-C or SIGTERM
Closes every websocket, writes the last chunk, closes the file.

What the challenge asked for

RequirementWhere it stands
Every live market, many websockets, one L2 book eachFull universe, about 450 sockets, flat sorted books
Deltas plus a snapshot every 60 s, the template's helpersPriceLevelUpdate, Snapshot, AlignedSbeWriter
Output to stdout; SIGINT closes sockets and fileBoth; SIGTERM and panics too
No MVEs (Kalshi)mve_filter=exclude
Ext 1: redundant websockets2 per shard on different edges, source arbitration, --supervise
Ext 2: subscription monitoringRate-aware resubscribe, quiet-socket reconnect, hourly recycle
Ext 3: generic encode/decodePolymarket and Kalshi adapters on one engine
Ext 4: latencyFour experiments on three hosts; see Latency

Each claim, with the command that reproduces it: GRADING.md. The design choices and the alternatives they beat: DECISIONS.md.

Every run, by host

Five stages, in the order they ran. Each move came from what the run before it showed. Every output file that was closed replayed with 0 mismatches.

1 · Laptop in India, behind a VPN

The link was the bottleneck

A 3.3 Mbit/s link to Singapore could not carry the start-up dumps of 131k tokens.

RunScaleResultFinding
Run 4131k tokens, 265 socketsfile cut offVPN collapsed into a reconnect storm
Run 647.5k of 131k got a book1.01M updates, 0 mismatchesA coverage gap (fix #2)
Run 7about 96k of 131k614k replayed, 0 mismatchesWire p50 130 s: move to the cloud (fix #1)
2 · GitHub Codespace, Azure West Europe, 4 vCPU

Scale, faults and a 2.5-hour soak

A few ms from Polymarket's London origin. Most of the fixes were found here, under the full universe and injected faults.

RunScaleUpdatesReplayFinding
Cloud 1250k of 446k4.44M0 / 2.27MThe universe is bigger; 429s (#3)
Cloud 2459k, all placed5.67M0Timers starved, core saturated (#4)
Cloud 3557k9.68M0 / 5.27MHash hot spot fixed (#5)
Soak 1, 90 min559k, 452 sockets185M0, 10 filesExt 2 over-tuned (#7)
Soak 2, 30 min560k64M0, 4 filesCore CPU is the limit (#8)
Profile 2561k18.6M0 / 10.3MRotation fsync stalls everything (#9)
A/B, 2 × 50 min566k108.6M ×20 / 110MHot-shard split loops (#10)
Soak 3, 2.5 h551k, 452 sockets320M0, 16 filesStable; 3 GB RSS, disk waits (#11, #12)
Final gate559k, kill -9 + SIGSTOP23.5M0, 7 filesEvery recovery path works
Perf gate 1563k, --pin --kernel-ts—0 / 9.67MRSS 1.73 GB; a --pin bug (#13, #14)
Perf gate 2561k, 1 io worker—0 / 10.8MOne io worker is too few (#15)

During the soaks: all 224 sockets to one edge were killed, the process was frozen for 15 s, and 448 sockets were recycled every hour. 535 disconnects healed on their own. The Codespace clock drifted up to 88 ms and its disk stalled 8 times, which is why the latency figures come from AWS.

3 · AWS, eu-west-2 London, c7i.xlarge

The measured run, with a synced clock

Launched, run and torn down by one script from the laptop, for about $0.50. Flags: --supervise --pin --kernel-ts --rotate-mins 10.

Coverage
548k–554k tokens on 442–452 sockets; 280,559 markets seen, 467 removed during the hour
Throughput
41–43k updates/s; 9 MB/s to disk, 5.4 GB per 10-minute file
Correctness
8 files, about 33 GB; about 55M snapshots matched, 0 did not
Latency
Exchange → collector p50 8–9 ms, p99 50–348 ms. Kernel → collector p50 ≤ 32 µs, p99 ≤ 128–256 µs
Resources
RSS 1.8 GB mean, 1.9 GB peak, flat. Core threads 47%, tokio 54%, writer 6% of one CPU
Writer
Queue peak 20 MB of 256; 0 waits on disk
kill -9 at 55 min
The supervisor restarted it in 1 s and kept the open file whole

The next day, five A/B tuning rounds on the same host type (see Latency), and three regions started at the same second for 10 minutes on 2,000 markets:

RegionUpdatesReplayp50p90p99
London, eu-west-2675k0 / 81,2887 ms21 ms118 ms
Ireland, eu-west-1680k0 / 82,05212 ms25 ms73 ms
Virginia, us-east-1679k0 / 82,00843 ms62 ms349 ms
4 · Kalshi demo environment, from the laptop

Kalshi, the challenge's venue, live

Kalshi needs a signed key even for public data, and production accounts are not open in India. The demo environment issues free keys. Same binary; only the URLs change.

RunMarketsSocketsLengthUpdatesSnapshotsMismatches
Smoke2001260 s80,0623320
Main1,0001215 min1,726,63329,8060
One subscription2,5002150 s276,55212,0060
5 · AWS, both venues at once, 2 hours

Two hosts, each next to its exchange

Polymarket from London on a c7i.xlarge, Kalshi's whole demo universe from Virginia on a c7i-flex.large, started together. Same flags, a kill -9 near the end of each. About $0.72 for both.

Polymarket, LondonKalshi demo, Virginia
Universe488k tokens, 392–400 socketsabout 100k markets, 162–166 sockets
Updates231.7M (31–34k/s)32.3M (3.4–4.9k/s)
Replay0 / 118.2M, 14 files0 / 48.9M, 14 files
recv − exch p50 / p995–6 ms / 19–128 ms33–36 ms / 43–49 ms
Host delay p50≤ 16–32 µs≤ 32–64 µs
RSS peak, load1.97 GB, 1.1 of 40.63 GB, 0.2 of 2
kill -9restarted in 1 s, open file recoveredrestarted in 1 s

Raw logs for every run are in bench/; the full story, turn by turn, is the engineering log.

How it got here

The first build was correct but small. Each run at a bigger scale or a harder fault found the next limit; each limit got a fix and a measurement. The charts follow the same metric across the runs, labelled with the fixes that landed before each one.

Reading the charts: each point is one run. The second line under a run names the fixes that came before it (numbers match the list below). Hover or tap a point for its value; each chart has a table view.

The fixes

In the order they were found. Each line says what hit the limit, how it was found, and what the fix moved.

  1. #1
    Laptop link behind a VPN, 3.3 Mbit/s
    Run 4 collapsed into a reconnect storm. Moved to a cloud host near London; added a global outage backoff.
    wire p99: minutes → 173 ms
  2. #2
    Coverage stuck at 47.5k of 131k tokens
    Coverage grew one shard per 30 s, then stopped. The open limiter now reads the current gap; books that never arrive are re-asked.
    47.5k → 96k, later 100% placed
  3. #3
    Socket cap and rate-limited lookups
    Cloud 1 left 196k tokens unplaced and 102 lookups got 429s. The cap is sized to the universe; lookups are batched with backoff.
    250k → 459k placed, 0 429s
  4. #4
    Timers starved by inbound messages
    Cloud 2 printed one status line in 5 minutes. Timers run first; inbound is taken in batches of 256.
    p99 71 s → 524 ms
  5. #5
    The hash check took 45% of the core
    A perf profile showed allocation per level and a full rehash per candidate price. Hash once, copy the partial state, format without allocating.
    18.9k → 32.3k updates/s
  6. #6
    One core thread at 99.9%
    Per-thread CPU from /proc. K core threads now each own whole markets.
    99.9% → 50–90% per core
  7. #7
    Ext 2 over-tuned
    Soak 1 made 400k resubscribes in 90 minutes. Quiet detection became rate-aware, and the wait doubles on each strike.
    resubscribes cut to the truly quiet tokens
  8. #8
    Hashing standbys and a full lag walk
    A whole-process profile. Only the source replica is hashed; the lag check keeps an incremental candidate list.
    core CPU 50–60% → 31–38%
  9. #9
    Rotation fsync stalled every reader
    Profile 2 had 330 s and 1,153 s blocked in rotation windows. A finisher thread fsyncs and renames off the writer.
    blocked time at rotation → 0
  10. #10
    Hot-shard splitting caused a reconnect loop
    The A/B showed 11–19× the disconnects: the venue closes a socket whose assets have all resolved. Resolved tokens are dropped; splitting is opt-in.
    loop gone
  11. #11
    Memory at 2.8–3.3 GB
    An offline bench found 7.6 KB of heap per token. mimalloc, flat sorted books, inline replicas, one symbol per token, lazy hash state, FxHash.
    RSS 3.0 → 1.73 GB
  12. #12
    Disk stalls could reach the books
    The 2.5-hour soak had 8 writer waits of 1–1.5 s. A byte-bounded 256 MiB writer queue absorbs them.
    8 waits → 0
  13. #13
    The receive time included TLS and parsing
    --kernel-ts reads the kernel's arrival time (SO_TIMESTAMPNS) and reports the host's own delay.
    host delay measured: p50 ≤ 32 µs on AWS
  14. #14
    --pin started one core thread instead of two
    Found in the gate log. CPUs are counted once, before pinning.
    2 core threads, 281k / 280k tokens
  15. #15
    One tokio worker for about 450 TLS sockets
    Host delay was 4× worse in gate 2. A 5-minute A/B of three layouts; io workers now default to at least 2.
    host p99 8–16 ms → 4–8 ms
  16. #16
    A kill -9 without the supervisor lost the file
    Chunks were written at the end. Each chunk is now written in place as it fills.
    data kept after kill at 120 s: 0 → 119.8 s

Ruled out by measurement: the TCP source port (Ext 4, Experiment 3), hyperthread siblings for the core threads, and the ~1% of tokens with no book (the venue has none).

How correctness is proven

The output carries its own proof. A snapshot of a side plus every update after it must equal the next snapshot of that side, exactly. verify rebuilds every book from the updates and checks this for every snapshot in a file; any mismatch exits non-zero.

1.2B+price-level updates written across the runs
0replay mismatches in any closed file
99offline tests, run in CI with clippy and a Docker build
12faults injected against mock venues, end to end
Offline
Unit tests; golden tests on captured live frames; 10 faults injected into a mock Polymarket (a dropped socket, a silent socket, a stall, a delay, a corrupt update, 429s, a resolution, a panic, kill -9, SIGTERM) and 2 into a mock Kalshi (a seq gap, a new listing), each beside a clean baseline. Each runs the real binary and checks its output with verify.
Live, on purpose
All 224 sockets to one edge killed at once; the process frozen for 15 s with SIGSTOP; kill -9 of the child under --supervise; all 448 sockets recycled every hour. Every file replayed with 0 mismatches.
Live, by the venue
535 disconnects in the Codespace soaks, 29 “slow consumer” closes in the AWS hour, and over 400 closes, resets and 429s across both venues in the two-hour run, each reconnected on its own while the peer replica kept the books; the files still replayed with 0 mismatches.
Reproducible
bench/aws/run.sh all launches the hosts, runs the gates, pulls the results and tears everything down for about $0.50. The raw logs of every run are in bench/.

The method

Measure at the next scale, find the limit, fix it, measure again. Every number on this page comes from a logged run, every fix names the run that found it, and an idea that did not beat the default stays off, with its result kept.

The decisions that shape it

Each design choice, the alternative it beat, and the evidence. All 21 are in DECISIONS.md.

Redundancy model
Choice
Two sockets per shard on different edge servers, each keeping its own book; one replica per token is the source.
Rejected
Merging first arrivals across sockets. Polymarket sends no sequence numbers, so dedup would be a heuristic that collapses two real, identical updates in the same millisecond.
Evidence
A failover target already holds a live, checked book, so a switch costs no round trip to the venue.
Source switch
Choice
Write the level changes from the old book to the new one as updates, then a snapshot.
Rejected
A snapshot alone. It hides whatever was lost before it: a reader cannot tell a switch from missing data.
Evidence
The snapshot-only first build gave 15.5k replay mismatches. With the diff: 0 in every file since.
Integrity check
Choice
A strike ladder on the venue’s SHA-1 book hash: resync only on a peer disagreeing or a second miss in a row. Only the source replica is hashed.
Rejected
Resync on every miss. Recomputing the hash is not exact (98.9% on one correct golden book), so 1% false misses on 566k tokens would be a resync storm.
Evidence
Golden captures 100% / 100% / 98.9%; hashing only the source cut core CPU by 38%.
Concurrency
Choice
Each core thread is a single-threaded state machine that owns whole markets, routed by hash. Time and side effects are injected.
Rejected
Arc<Mutex<Book>> per token: locks on the hot path and no single order of events per market.
Evidence
Event order is correct by construction, and 48 unit tests drive the core directly. One core saturated at 560k tokens; two run at 31–38%.
Output layout
Choice
Two symbols per token, <token>:bid and :ask; 64 MiB chunks written in place as they fill.
Rejected
Inferring the side from the price (ambiguous inside the spread); other chunk sizes (the template’s reader only reads 64 MiB).
Evidence
Lossless, and byte-compatible with the template reader. A kill -9 at 120 s kept 119.8 s of data.
Disk off the hot path
Choice
A writer thread behind a 256 MiB byte-bounded queue; a second thread fsyncs and renames rotated files.
Rejected
Inline fsync at rotation.
Evidence
Blocked time in two rotation windows: 330 s and 1,153 s → 0 in all 11 rotations. Disk waits in the soak: 8 → 0.
Crash handling
Choice
A panic shuts the process down in order and exits non-zero; --supervise restarts it, trims torn chunks and marks the gap with a reset snapshot.
Rejected
Restarting a dead core in-process (its sockets, books and writer source would all need rebuilding); systemd alone (it cannot repair a torn chunk or mark the gap).
Evidence
3 injected panics → 3 restarts, verify OK. On AWS, kill -9 at 55 min → back in 1 s, file intact.
Numbers
Choice
Decimal strings parsed straight to i64 (1e6 = 1.0). More than 6 dp is rejected loudly.
Rejected
Parsing through f64.
Evidence
Every Polymarket price and size is exact; nothing is rounded.
Clock
Choice
Measure the host clock offset over SNTP and log it; leave steering to chrony.
Rejected
Correcting received_at by the offset: a step mid-run would reorder timestamps.
Evidence
On AWS, chrony held 0.4–9 µs RMS; the Codespace’s drift of tens of ms was caught this way.

Built, measured, and kept off

Four ideas were built in full and A/B tested. The data did not justify them as defaults, so each stays as an opt-in flag and the null result is written down.

IdeaWhat the A/B showedDefault
Hot-shard splitting11–19× the disconnects: the hot markets are 5–15 minute crypto markets that resolve right after a split. It also exposed a reconnect-loop bug, now fixed.off, --split-ratio
Speed-aware sourceThree A/Bs, the last on the full universe on AWS: mean 9.1 → 8.8 ms, p99 52 → 50 ms, for 4.8× the switch snapshots.off, --fast-source
1 s failover window0 false switches at 2,000 markets, but no faster delivery; the 3 s margin stays for the full universe’s stalls.3 s, --lag-ms
Kernel busy pollingThe second “off” arm matched “on”: the difference was venue drift.off

Where the time goes

Three numbers, each with a different clock problem. Only the last one is time spent inside the collector.

recv − exch
Kernel receive time minus the venue's event timestamp. Includes the venue's own processing, the network, and any clock offset, so it needs a synced clock (chrony on AWS).
behind
How long after the first socket to deliver an event another socket delivered it. Both times are on our clock, so it compares sockets and edges without any offset.
Host delay
Kernel receive to the collector reading the frame (--kernel-ts). This is the part the collector controls.
8–9 msrecv − exch p50, full universe, AWS London
≤ 32 µshost delay p50, every 30 s window
≤ 8 mscore queue p99

The four experiments

1 · Many sockets, same tokens
Eight sockets on 400 markets for 20 minutes, two edges. The best socket was 1.4 ms behind the first on average and the worst 10.3 ms; the faster edge was about 2.5 ms better. built in
Replicas use different edges, and the source is the replica that is ahead.
2 · A speed-aware source
--fast-source scores sockets and prefers the faster one. It moved means by tenths of a ms and made one p99 worse. opt-in
3 · Does the source port pick the path?
Twelve sockets over six rounds, six on fixed ports. Rank correlation round to round +0.27, p ≈ 0.10: no reliable effect. ruled out
4 · Tuning on AWS
Five A/B rounds: --fast-source, 4 replicas, a 1 s failover window, kernel busy polling. The best gain was under 1 ms at p50. defaults kept

Experiment 4, round by round

RoundArmWritten p50 / p99Behind mean / p99
r1, full universe, 20 mindefault5 / 52 ms1.5 / 13 ms
--fast-source5 / 50 ms1.2 / 11 ms
r2, 2,000 markets, 15 min--fast-source5 / 30 ms0.4 / 3 ms
+ --replicas 45 / 31 ms0.7 / 5 ms
r3, 2,000 markets, 15 min--lag-ms 3000 (default)5 / 36 ms1.1 / 15 ms
--lag-ms 10005 / 28 ms0.8 / 6 ms

Conclusion. The collector adds microseconds; the milliseconds are the venue and the path to it. The one lever that moved the median by a lot is the region: London 7 ms, Ireland 12 ms, Virginia 43 ms on the same markets at the same second.

Every round, table and raw log: LATENCY.md.

Kalshi, the challenge's venue

The challenge names Kalshi and allows Polymarket as the fallback. --venue kalshi runs the same core, socket actor, writer and verify on a Kalshi adapter (Ext 3). The adapter is the only part that knows Kalshi exists.

PolymarketKalshi
Authnone for market dataa signed handshake: RSA-PSS or Ed25519, fresh per connect
Updatesabsolute size at a pricerelative deltas, turned into absolute sizes by the adapter
Gap detectionthe venue's book hashseq per subscription; a gap triggers a resync
Universeabout 550k tokensMVEs excluded with mve_filter=exclude
Symbols<token>:bid, :ask<ticker>:yes, :no bids

How it was tested

Unit tests
Kalshi's example frames from its docs; signatures with both key types checked over the exact signed string.
End to end, offline
A mock Kalshi venue: the real binary, two replicas, then verify. Cases: a baseline, a seq gap, a new listing.
Live, demo environment
1,000 markets for 15 minutes: 1,726,633 updates, 29,806 snapshots replayed, 0 mismatches, 0 disconnects, 0 seq gaps. Also 2,500 markets on one subscription.
Live, two hours from AWS
The whole demo universe, about 100k markets, from Virginia: 32.3M updates, 48.9M snapshots replayed, 0 mismatches; recv − exch p50 33–36 ms against 214–222 ms from India.
Both venues together
./demo.sh both: one process per venue, side by side; each file replays with 0 mismatches.

Environment. The live runs used Kalshi's demo environment, which serves the same websocket API and message formats as production. Pointing it at production is the default: ./demo.sh kalshi with a production key.

Details: KALSHI.md.

Run it

Needs stable Rust. One command builds, replays the committed sample, runs the offline tests and collects two minutes of live Polymarket data:

./demo.sh

Polymarket, by hand

cargo build --release --bins
./target/release/collector --max-markets 2000 --duration-secs 120 > ob.sbe
./target/release/verify ob.sbe

The output goes to stdout by default and status lines to stderr. --out ob.sbe writes the file directly; rotation needs that form.

Kalshi

Put a key in a gitignored .env (KALSHI_API_KEY_ID, KALSHI_PRIVATE_KEY_PATH), then:

./demo.sh kalshi                # production
KALSHI_DEMO=1 ./demo.sh kalshi  # demo environment
./demo.sh both                  # both venues at once

Elsewhere

Docker
docker build -t collector ., then run it with a volume for the output.
GitHub Codespace
The repo has a devcontainer for a 4-core machine; run ./demo.sh in it.
AWS
bench/aws/run.sh all launches, runs, pulls and tears down the measured run.
A Linux service
systemd plus --supervise --rotate-mins 10; sizing for 4 vCPU and 8 GB.

HOW_TO_RUN.md GRADING.md Source