Skip to content
RT/SOFT
journal — cat credit-windows-for-quote-fanout.md
F3

← journal·2026-09-26

Credit Windows for Quote Fan-Out

Not financial advice. Verify claims independently.

Every realtime post eventually says "apply backpressure." Almost none of them say what the server is allowed to forget. Market data forces the question because the messages are not equal. A quote is a suggestion that expires. A trade is a fact. A halt is a fact the UI is not allowed to miss. A credit window that treats them as one queue will either stall the shard or drop the wrong frame.

WebSocket does not help you. The protocol has no message-level credit. Bytes buffer in the kernel, memory climbs, and the only portable client signal is WebSocket.bufferedAmount, which tells you the sender is ahead of the network. It does not tell you which of those buffered messages still matter. SSE inherits HTTP flow control at the connection. WebTransport inherits QUIC flow control, which is better: a slow stream stops accepting data instead of pretending the socket is infinite. Flow control still only answers "can I send." The fan-out has to answer "what is worth sending."

A window with a policy

We give each subscriber connection a fixed credit, counted in frames, not in bytes alone. Bytes matter for memory. Frames matter for meaning. When credit is available, the connection drains in priority order:

  1. Control frames. Heartbeats, acks, slow-consumer notices, resubscribe confirmations. A client that cannot hear "you are behind" will retry blindly and make the outage worse.
  2. Trades, halts, and corrections. Never conflated. If credit is tight they occupy it first, and quotes wait.
  3. Quotes, coalesced to the latest value per symbol. Ten updates behind a slow phone become one update. The book is current. The path those ten ticks took is gone, and that is acceptable.

When credit hits zero we do not grow the queue to be kind. Kindness is how a single stuck tab pins a shard. We keep one slot per symbol for the latest quote, outside the credit count, and we increment a drop counter the dashboard already shows. The client receives a slow-consumer frame with the drop count so the SDK can shed chart work, freeze animations, and request a snapshot instead of replaying a minute of tape into React.

QUIC does not replace this. If you let flow control block the producer, a slow consumer stalls every symbol that hashes to that worker. Credit plus conflation keeps the shard moving. Flow control remains the safety net for a client that has stopped reading entirely. You want both. You do not want flow control to be your product policy.

Numbers you can argue with

A symbol at 200 messages per second is not a UI requirement of 200 paints. A 100 to 250ms coalesce on the client turns that into a handful of paints per second per row. The server can coalesce even harder for a connection that is already marked slow, down to one quote per symbol per 500ms, while trades still pass immediately. Publish the thresholds. A latency SLO that hides its drop policy is a marketing number.

Heartbeats belong in the same budget. A 30-second ping is a tutorial default. Carrier-grade NAT will forget a quiet flow well before that, and a stalled credit window looks identical to a dead socket if you are not measuring round trip. We run a short application heartbeat, on the order of a few seconds, and a missed round trip is a soft reconnect: keep the last book on screen, refresh interest, snapshot, then resume. A hard drop blanks the UI and trains users to think the feed is down when only the phone slept.

Symbol ownership still comes first. Credit is per connection, but the worker that owns the symbol is per shard. A connection subscribed to symbols on three shards has three credit conversations, or you will invent a cross-shard queue and reintroduce head-of-line blocking in the application. The hash that places a symbol does not move because one subscriber got slow. Resharding moves virtual nodes. It should not reset every credit window on the edge, or a deploy looks like a market-wide stall.

What the phone should do with the signal

A slow-consumer frame is useless if the client treats it as a log line. The SDK should:

  • Stop per-tick React renders and paint from the latest coalesced map on the next animation frame.
  • Refuse to enqueue an unbounded history so the UI can catch up. Catch-up is a snapshot, not a replay.
  • Surface a small stale marker if drops cross a threshold, then clear it when credit recovers. Silent degradation is how a paper P&L lies.

You can watch that behavior without standing up a shard. Stock Picks is the public consumer on the other end of this fan-out: paper positions, a quote path that coalesces, and a screen that should stay up when the phone is the slow one. Background the app, come back, and see whether you get a snapshot or a freeze. If you get a freeze, the credit window exists only in a design doc. The dashboard drop counter is the other half of the same claim. If it stays at zero forever, nobody is telling the truth about load.

next — practice on live feed
NEXT

Put it on a live wire

Stock Picks runs on RT/SOFT — open the paper app and watch the same fan-out path that powers this latency dashboard.

$ open Stock Picks →
Link up│Fanout 14ms│Msg/s 98,34000:00:00 ET