WebSocket Lab

One upstream connection, many downstream. This site’s server holds one WebSocket to Hyperliquid’s public market-data feed and fans every price it receives out, over a WebSocket of its own, to everyone who has this page open. Below are the live prices, what your own connection has received, a load test you can start against the same hub, and the same hub measured at 10,000 to 100,000 subscribers.

This is market data on display, to show how fanout behaves. It is not advice and not a recommendation about any asset.

Live prices

Connecting to this page’s hub…

Best bid and best ask on Hyperliquid’s BTC, ETH, SOL perpetual markets, exactly as the feed sent them
SymbolBest bidBest askFeed’s timestampUpdates seen

Market data: Hyperliquid public API (wss://api.hyperliquid.xyz/ws, the bbo channel). Shown as received; this site computes no price of its own.

Your connection

Counted by this tab from what actually arrived on its socket.

State—
Connected for—
Frames received—
Prices received—
Prices replaced before they reached you—
Round trip to the hub—

Load test

Start a test and the server opens that many real WebSocket subscribers to this same hub over its own loopback interface, holds them for 15 seconds, and measures what they receive. Every visitor on the page keeps receiving the same feed throughout.

Simulated subscribers

No test is running.

  • One test runs at a time across the whole site; a second start while one is running is refused.
  • Sizes offered: 50, 250, 500 subscribers, at most 500. One address can start a test every 120 seconds.
  • While a test runs, one more subscriber stands in for a real visitor and is watched: if its p99 delivery latency over the last 2000 ms passes 250 ms, the test stops early and says so. It also stops if the server’s heap passes 384 MiB.
  • A test cannot start while the feed is down: there would be nothing real to send.

Why at most 500: this site runs on one virtual CPU, and nearly all the cost of a subscriber is the kernel’s work for one socket write per message at the hub and one read at the subscriber — both inside this one process during a test. The cap keeps a test well inside that one CPU; the watched connection is what stops it if real visitors would notice anyway.

The same hub at scale

Run with docker compose from the public repository at v0.1.0: the hub and the load generator on one machine, each level against a freshly started hub, read from the committed results.

Machine: Intel(R) Core(TM) i7-8650U CPU @ 1.90GHz, 8 logical CPUs, 61 GiB of memory, Linux 7.0.0-38-generic, go1.27.2. Date: .

docker compose on one laptop: the hub in one container listening on four ports, the load generator in another, each level against a freshly started hub, measured for 60 s after every subscriber had connected.

Each row is one run, measured over its window after every subscriber had connected
SubscribersConnectedDropped p50p95p99Slowest Delivered / sFeed / sReplaced Hub heap peak (MiB)Hub goroutines peakHub CPUs busy
10,00010,0000 120 ms250 ms320 ms449 ms 93,70411.91,594,439 30820,0123.7
50,00050,0000 700 ms2,000 ms2,000 ms2,114 ms 152,08212.528,968,421 1,492100,0123.9
100,000100,0000 2,000 ms3,000 ms4,000 ms4,238 ms 137,66612.569,727,886 2,949200,0123.6
  • The hub and the load generator share one machine's 8 logical CPUs, so each level measures the two together. Most of the CPU time on both sides is the kernel's TCP work — one write per frame at the hub, one read per frame at the subscriber (profiled in development, not recorded in these files).
  • The machine was a laptop with a desktop session running, using about one CPU of its own during the runs.
  • The feed's rate is the market's: each level ran at a different time and saw a different number of prices from the feed, recorded per run as feed/s. Levels are therefore comparable in what the hub did with what arrived, not in identical input.
  • Latency is measured from the moment the hub read a price from the feed to the moment a subscriber read it, on one clock. The time the price spent between the exchange and this machine is not included.

Method

One upstream, many downstream. The server keeps a single connection to the feed, subscribed to the best bid and offer for BTC, ETH, SOL. Each message is stamped with the moment the hub read it and a per-symbol sequence number, encoded once, and offered to every subscriber. If the feed drops, the hub reconnects with a doubling wait, the page says since when the feed has been down, and the last real price stays on screen with the feed’s own timestamp. Nothing is ever filled in.

Conflation for slow subscribers. Each subscriber has a mailbox holding at most one unsent price per symbol. A newer BTC price replaces an older one that has not been written yet, so a subscriber that falls behind receives the latest price rather than a growing queue, and the hub never waits for it. The sequence numbers let a subscriber count what it skipped — that is the “replaced before they reached you” row above. Replacement can happen even with no load at all: the feed sometimes sends two updates for one symbol back to back.

Drops, with a reason. A write that takes longer than 5 seconds drops that subscriber; so does one whose prices have waited 10 seconds with nothing written. The hub sends the reason before closing when it can, and counts every drop by reason. The hub pings each subscriber every 25 seconds, under the 60-second idle limit of this site’s proxy. One address may hold 4 live-feed connections, and the page as a whole 300.

What is measured, and how. Delivery latency is a subscriber’s own receive time minus the moment the hub read that price from the feed. That is only meaningful when both clocks are the same clock, so it is measured only where they are: the load test’s subscribers run inside this server, and the at-scale runs put the hub and the load generator on one machine. Latencies are kept in fixed buckets — ten microseconds wide below one millisecond, ten times wider in each decade above it; a percentile reports its bucket’s upper edge, so it is never understated, and the slowest delivery is exact. Your own tab cannot share the server’s clock, so for your connection the page measures a round trip instead — a ping it sends and the hub echoes — and does not claim a one-way figure.

The rate is the market’s, not the test’s: the feed sends an update when the best bid or offer changes, so a quiet hour sends fewer prices than a busy one. Every result above states how many prices the feed sent during its own window.