Low-Latency Software · Technology
17WebSocket, REST, TLS and JSON at Speed
Crypto venues and many brokers do not speak FIX or binary protocols to most of their clients: market data arrives as JSON text inside WebSocket frames inside TLS records, and orders leave as signed HTTP requests or as JSON over the same kind of connection. A trading server placed in the venue’s cloud region (One Quant Book 3, chapter 26) sits a fraction of a millisecond from its matching engine. On this book’s laptop, the Python reference of this chapter’s build spends almost of processor time to receive one depth update and send one order (opening and sealing TLS records, unmasking and masking frames, decoding the JSON, signing); the C++ components spend about one. The first is a tenth of a half-millisecond round trip; the second disappears in it. This chapter takes the web stack apart layer by layer: HTTP and the cost of opening connections, TLS and what its handshake and records cost, WebSocket framing, JSON decoding with a structural index, and request signatures computed without rehashing the key.
17.1 HTTP, REST and connection reuse
Definition 17.1 (REST interface, persistent connection, connection pool)
A REST interface exposes a service as resources addressed by URLs and manipulated by HTTP requests (GET to read, POST to create, DELETE to remove), each request carrying everything the server needs to process it. A persistent connection carries many HTTP requests and responses over one TCP (and TLS) connection instead of opening one per request. A connection pool keeps a set of open persistent connections to a server and lends an idle one to each request.
A crypto venue’s order entry is typically a REST interface: an order is a POST with the order’s parameters, the client’s API key and a signature (One Quant Book 3, chapter 15), and the answer is JSON. The expensive part is not the request but the connection under it. Opening one costs a TCP handshake, one round trip, then a TLS handshake, another round trip plus the cryptography of key exchange and certificate verification; a WebSocket adds an HTTP upgrade, a third. HTTP/1.1 “defaults to the use of persistent connections” (RFC 9112), and a client that uses them pays these costs once per connection instead of once per order. The build’s pool keeps a fixed number of connections open and passes every request through Book 3’s rate-limit governor first, since a venue’s limits count requests and weights (Book 3, chapter 15), not connections.
17.2 TLS: the handshake and the record layer
Definition 17.2 (TLS handshake, session resumption)
The TLS handshake is the exchange that opens a TLS connection: the two sides agree on a protocol version and cipher, exchange key shares, the server proves its identity with its certificate, and both derive the keys that protect the records that follow; in TLS 1.3 it takes one round trip. Session resumption opens a new connection with a key derived from an earlier one (a pre-shared key sent to the client as a ticket), skipping the certificate and part of the key exchange.
TLS 1.3 (RFC 8446) cut the full handshake to one round trip and replaced the older session identifiers and tickets by resumption with a pre-shared key: “the key derived from the initial handshake is used to bootstrap the cryptographic state instead of a full handshake”. A zero-round-trip mode lets a resuming client send data with its first message, “at the cost of certain security properties” (the data can be replayed), which is why an order must never travel in it. After the handshake, the record layer cuts the stream into records of at most bytes and protects each with an authenticated cipher.
Table 17.1 gives the costs on the laptop’s loopback, where the network costs almost nothing and what remains is processing. A full TLS 1.3 handshake with an elliptic-curve certificate takes about with Python’s ssl module (both ends on the same machine), a resumed one about . A round trip of 300 bytes on an open connection takes about over TLS and over plain TCP, both in Python; the protection itself, one AES-128-GCM seal or open of a 300-byte record with OpenSSL called from C++, is under . The handshake is ten thousand times dearer than a record: the reason to keep connections open.
| Operation (loopback, laptop) | Cost |
|---|---|
TLS 1.3 full handshake (Python ssl, both ends) | |
TLS 1.3 resumed handshake (Python ssl, both ends) | |
| Round trip of 300 bytes over TLS (Python) | |
| Round trip of 300 bytes over plain TCP (Python) | |
| AES-128-GCM seal of a 300-byte record (OpenSSL, C++) | |
| AES-128-GCM open of a 300-byte record (OpenSSL, C++) |
bench_web.py, measured_tls.csv and measured_web.csv.17.3 WebSocket framing
Definition 17.3 (WebSocket, WebSocket frame)
A WebSocket is a bidirectional message channel over one TCP connection (usually inside TLS), opened by an HTTP request that asks to upgrade the connection (RFC 6455). A WebSocket frame is its unit on the wire: a two-byte header (a final-fragment bit, an opcode for text, binary, continuation, close, ping or pong, a mask bit and a 7-bit length), an extended length of 2 or 8 bytes when the payload exceeds 125 bytes, a 4-byte masking key on frames from the client, and the payload.
Three rules of RFC 6455 shape a client. “A client MUST mask all frames that it sends to the server”, with a new random key per frame, even over TLS: the payload is XORed with the key repeated, a defence of intercepting proxies, not of the data. A message may be split into fragments (a first frame with the opcode, continuation frames, the last with its final bit), and control frames (ping, pong, close) may arrive between fragments, carry at most 125 bytes and are never fragmented. And the venue checks the connection with pings that the client must answer: one large venue sends a ping every 20 seconds and drops a connection that has not answered within a minute. The build’s decoder unmasks in place eight bytes at a time.
// XOR with the 4-byte key, eight bytes at a time (the key repeated twice in a 64-bit word), then the tail.
inline void unmask(std::uint8_t* p, std::size_t n, const std::uint8_t key[4]) {
std::uint64_t k8;
std::uint8_t kk[8] = {key[0], key[1], key[2], key[3], key[0], key[1], key[2], key[3]};
std::memcpy(&k8, kk, 8);
std::size_t i = 0;
for (; i + 8 <= n; i += 8) {
std::uint64_t w;
std::memcpy(&w, p + i, 8);
w ^= k8;
std::memcpy(p + i, &w, 8);
}
for (; i < n; ++i) p[i] ^= key[i % 4];
Framing costs little in C++: about to decode and unmask a 300-byte frame, to encode and mask one. The build’s Python reference, which XORs the payload one byte at a time, takes about for the same frame: several hundred times more, and the largest item of its budget.
17.4 JSON at speed
Definition 17.4 (Structural index)
A structural index of a JSON text is the array of the positions of its structural characters (braces, brackets, colons, commas) and quotes, computed in one vectorised pass; a decoder then walks the index instead of the bytes, reading each value directly between two known positions.
The parser of Langdale and Lemire (chapter 14) works in two stages: the first writes “the location of all structural characters … as integer indexes in a separate array”, the second walks that index. The build applies the idea to one message shape, the depth update a large venue documents for its diff-depth stream: an event time, a symbol, the first and last update identifiers (U, u) that Book 3’s book builder checks for gaps, and arrays of bids and asks as [price, quantity] strings. The first stage is chapter 14’s positions_any over the seven characters {}[]:, and the double quote; the second walks the positions with the shape known in advance, reads each number where it stands, and parses prices into integers of without floating point. Anything unexpected (an escape, a missing key) makes it return false, and a general parser takes over.
while (k < m) {
std::string_view key;
if (!str(key) || k >= m || s[ix[k++]] != ':') return false;
if (key == "b" || key == "a") {
if (!(key == "b" ? levels(d.bids, d.nb) : levels(d.asks, d.na))) return false;
} else if (s[ix[k]] == '"') {
std::string_view v;
if (!str(v)) return false;
if (key == "s") {
const std::size_t n = v.size() < sizeof d.symbol - 1 ? v.size() : sizeof d.symbol - 1;
std::memcpy(d.symbol, v.data(), n);
d.symbol[n] = 0;
}
} else { // a number: from after ':' to the next ',' or '}'
const std::size_t a = ix[k - 1] + 1, b = ix[k];
std::uint64_t v = 0;
std::size_t used = 0;
if (!simdscan::parse_uint(s + a, b - a, v, used) || used != b - a) return false;
if (key == "E") d.event_time = v;
else if (key == "U") d.first = v;
else if (key == "u") d.last = v;
}
const char c = s[ix[k++]];
if (c == '}') return k == m;
if (c != ',') return false;
}
return false;
}
On the fixture’s 1 000 depth updates (about 286 bytes each, up to ten levels a side), the indexed decoder takes about per update; a general parser that builds a tree of maps, vectors and strings and converts prices with strtod about , seven times more; Python’s json module and the conversion to integers about . The decoded levels, divided by the instrument’s tick and lot, are what Book 3’s firm.wsbook applies.
17.5 Signing requests off the hot path
Definition 17.5 (Request signature)
A request signature is a message authentication code computed by the client over a request’s parameters with a secret shared with the venue (typically HMAC-SHA-256 of the query string and body, sent in hexadecimal with the API key), which lets the venue check that the request comes from the key’s owner and was not altered; a timestamp and a validity window inside the signed payload stop old requests from being replayed.
HMAC (RFC 2104) computes . Both padded keys are one 64-byte block, so the hash’s state after absorbing each of them depends only on the key; the RFC notes that these intermediate results “can be precomputed only once at the time of generation of the key”. A signer that does so absorbs the key once, copies the two states for each request and hashes only the message. The build’s signer does this in C++, Rust and Python, and is checked against RFC 4231’s vectors and against the worked example of a large venue’s documentation, whose published key and order payload give a published signature.
class HmacSha256 {
public:
explicit HmacSha256(std::string_view key) {
std::uint8_t k[64] = {}, pad[64];
if (key.size() > 64) {
Sha256 h;
h.update(reinterpret_cast<const std::uint8_t*>(key.data()), key.size());
h.final(k);
} else {
std::memcpy(k, key.data(), key.size());
}
for (int i = 0; i < 64; ++i) pad[i] = k[i] ^ 0x36;
inner_.update(pad, 64);
for (int i = 0; i < 64; ++i) pad[i] = k[i] ^ 0x5c;
outer_.update(pad, 64);
}
void sign(std::string_view msg, std::uint8_t out[32]) const {
Sha256 in = inner_, o = outer_;
in.update(reinterpret_cast<const std::uint8_t*>(msg.data()), msg.size());
std::uint8_t ih[32];
in.final(ih);
o.update(ih, 32);
o.final(out);
The costs teach two lessons. Precomputing the pads saves about two-fifths with the build’s portable SHA-256 (about against for a 119-byte order), and four-fifths with OpenSSL (about against for its one-shot HMAC()). And the library is several times faster than a portable implementation because it uses the processor’s SHA instructions: cryptography is the one place where the fast path is someone else’s code, called well.
As of September 2026 — A venue’s signing and streams
Binance’s spot API documentation (GitHub, consulted September 2026) signs each request with HMAC-SHA-256 of the query string and body, keyed by the API key’s secret, and bounds its validity with a timestamp and a window, recvWindow, of 5 000 milliseconds by default and 60 000 at most. Its WebSocket streams send a ping every 20 seconds, drop a connection that has not answered within a minute, and end any connection after 24 hours; its diff-depth stream publishes updates every 100 or 1 000 milliseconds.
Figure 17.2 puts the pieces together: the processor time of receiving one depth update and sending one order, step by step, for the build’s C++ components and for its Python reference.
ssl (its record cost estimated as a quarter of the extra round-trip time over TLS). Measured on a laptop (Intel Core Ultra 7 155H) under WSL2, no isolated cores. Data: bench_web.py.17.6 Tutorial: the web stack, measured
Goal. Build and check each layer, then measure the cost of one websocket order. End state: Table 17.1, Figure 17.2, and green tests in Python, C++ and Rust.
- Frames. The tests encode RFC 6455’s own examples (an unmasked and a masked “Hello”, lengths needing 16 and 64 bits) and decode the fixture stream written by
make_ws_fixtures.py: fifty depth updates, some fragmented in three with a ping between the fragments, and a close. Python, C++ and Rust deliver the same messages in the same order. - Depth updates. The C++ indexed decoder reproduces, level for level, the Python reference’s decoding of the fixture’s 1 000 updates.
- Signatures. All three signers reproduce RFC 4231’s test cases and the venue’s published example.
- TLS.
bench_web.pymakes a self-signed certificate with theopensslcommand, runs a TLS 1.3 server in a thread, and times full and resumed handshakes (it checks that every resumption was accepted) and round trips.
What to change next. Unmask with 32-byte vector XORs (chapter 14) and measure a 16-kilobyte frame; replace the Python reference’s masking with a bytes-level XOR over integers and measure again.
17.7 Build: the web client
Purpose. The firm’s connection to venues that speak JSON over WebSocket and REST: depth updates decoded for Book 3’s firm.wsbook, orders signed and sent through a pool of persistent connections that respects the venue’s rate limits.
Interface. Python reference firm_wsclient: encode_frame, decode_frame, Reassembler, decode_depth, to_book_levels, Signer, Pool. C++20 firm::ws: encode_frame, decode_frame (unmasking in place), Reassembler, decode_depth into a fixed Depth, Sha256, HmacSha256. Rust firm_wsclient: encode_frame, decode_frame, Sha256, HmacSha256.
Rules. Client frames are always masked; control frames are never fragmented and carry at most 125 bytes; lengths are minimally encoded; prices and quantities are integers of , never floats; the key’s pads are hashed once; the pool never opens more than its size and asks the governor before every request.
Acceptance tests. code/firm/wsclient/: RFC 6455’s frame examples; the fixture stream reassembled identically in the three languages; the 1 000 fixture updates decoded identically in Python and C++; RFC 4231 test cases 1, 2 and 6 and the venue’s example signature in the three languages; the pool’s reuse, capacity and throttling.
Stretch. Vectorised unmasking; a general structural-index JSON reader; per-message deflate; the order path through the simulator’s crypto preset of One Quant Book 10.
Sources and further reading
- IETF RFC 6455 (WebSocket), RFC 8446 (TLS 1.3), RFC 2104 (HMAC), RFC 4231 (HMAC-SHA-256 test vectors), RFC 9112 (HTTP/1.1).
- G. Langdale and D. Lemire, “Parsing gigabytes of JSON per second”, The VLDB Journal 28(6), 2019.
- Binance spot API documentation (GitHub): REST request security, WebSocket streams.
17.8 Exercises
Exercise 17.1 ★
How many bytes does a client frame carrying 300 bytes of payload occupy? And 70 000 bytes? And a server frame of 100 bytes?
Solution
Solution of Exercise 17.1.
bytes (header, 16-bit length, masking key, payload); bytes; a server frame is not masked, so bytes.
Exercise 17.2 ★
A REST order on a new connection pays TCP and TLS setup. With a round trip of to the venue and the measured handshake, how much later does it arrive than on a persistent connection?
Solution
Solution of Exercise 17.2.
Two round trips (TCP, then TLS) and the handshake’s computation: with a full handshake, about with a resumed one. On the measured laptop the handshake’s computation, which both ends share, costs more than the round trips.
Exercise 17.3 ★
Why must a client never send an order in TLS 1.3’s zero-round-trip data?
Solution
Solution of Exercise 17.3.
Zero-round-trip data can be replayed: RFC 8446 buys the saved round trip “at the cost of certain security properties”. An order replayed by an attacker or a retransmitting middlebox would be executed twice; orders must travel only after the handshake completes.
Exercise 17.4 ★★
Why can the two padded keys of HMAC be hashed in advance, and how many SHA-256 compressions does signing a 119-byte payload take with and without that precomputation?
Solution
Solution of Exercise 17.4.
Each padded key is exactly one 64-byte block that depends only on the key, so the hash state after it is the same for every message. Without precomputation: the inner hash takes the ipad block and the 119-byte message padded to two blocks (three compressions), the outer the opad block and the 32-byte inner digest padded to one (two): five. With it: three.
Exercise 17.5 ★★
A depth update’s price is the string 64123.2. What does the indexed decoder return at , and what can go wrong if a decoder converts it with strtod and multiplies by ?
Solution
Solution of Exercise 17.5.
6 412 320 000 000 exactly. In binary floating point 64123.2 is not representable; multiplied by it lands a hair below or above the integer, so truncation can give 6 412 319 999 999, and prices that differ in the last digit can compare equal or unequal by accident. Parsing the decimal text into an integer avoids the question.
Exercise 17.6 ★★
A ping arrives between the first and second fragments of a depth update. What must the client do, and in what order?
Solution
Solution of Exercise 17.6.
Answer the ping at once with a pong carrying the same payload, keep the first fragment, and deliver the depth update only when its final fragment arrives; control frames may interleave with fragments but never split them.
Exercise 17.7 ★★★
Coding. Replace the Python reference’s byte-by-byte masking with one XOR of two large integers built with int.from_bytes, check the fixture tests, and measure the frame costs again.
Solution
Solution of Exercise 17.7.
Repeat the key to the payload’s length and turn both byte strings into integers (int.from_bytes); XOR the two integers and convert the result back to bytes (to_bytes). The fixture tests are unchanged; the cost per 300-byte frame falls from tens of microseconds to a few.
Exercise 17.8 ★★★
Find the flaw. “Each strategy thread opens its own HTTPS connection for each order, so that no two threads ever contend for a connection.”
Solution
Solution of Exercise 17.8.
Every order pays two round trips and a handshake, about here, the venue sees a new connection per order (and may limit or ban them), and the rate-limit governor has no single place to count requests. A pool of persistent connections shared behind the governor avoids all three; contention for a connection costs nanoseconds, a handshake milliseconds.
17.9 Problem: Cloud to Venue
Problem 17.1
Weekend problem — where the time of a websocket order goes
A strategy runs in the same cloud region as a crypto venue. Assume a one-way network hop of and a decision of negligible cost; take the measured costs of Figure 17.2 and Table 17.1 (measured_budget.csv, measured_tls.csv).
Part I — The budget.
- What is the total processor time of one order with the C++ components, and with the Python reference?
- Which step dominates each?
- What share of the path venue, host, venue is the host’s processing, in each case?
- What would the Python share be with the C++ framing alone (the rest unchanged)?
Part II — Connections.
- What does opening a new connection for each REST order add, with a full handshake, and with a resumed one?
- What does a new WebSocket add before its first message?
- How many orders a second could one connection carry if each took a new connection?
- What does the venue’s 24-hour connection limit imply for a strategy that runs for days?
Part III — Signatures and data.
- Why can the signature not be computed before the decision, and what can be?
- At 100 depth updates a second per instrument over 50 instruments, how much of a core does decoding take with each decoder?
- What does the update identifier pair
(U, u)let the client detect, and what does it do then? - Why is the timestamp inside the signed payload?
Part IV — The verdict.
- State the named result: the share of the tick-to-trade spent in decryption, framing, JSON and signing against the network hop, and what connection reuse saves per order.
- When does the Python reference suffice?
- Where would you put the TLS work if the budget were tighter still?
- What would you measure on the real server before trusting these laptop numbers?
- Why does the network hop not appear in the processor budget, and why does it still matter?
- What part of this stack does a venue’s binary protocol remove?
- How would you test the pool’s behaviour when the venue answers 429?
- In one sentence: what is fast about a web stack that is fast?
Solution
Solution of Problem 17.1.
- On the committed measurement, about with the C++ components and with the Python reference.
- C++: the JSON decoding (). Python: the byte-by-byte framing, to unmask and to mask.
- and .
- About , of the path: framing was two-thirds of the Python cost.
- of round trips plus the handshake: about (full) or (resumed) per order.
- Three round trips and the handshake, about .
- Opened one after the other, about 330 a second; on a persistent connection the limit is the venue’s rate limit.
- Plan a reconnection before the limit: open the new connection, subscribe and resynchronise the book, then close the old one, so that no update is missed.
- It covers the order’s price and quantity, known only after the decision. The keyed hash state, the fixed parts of the payload and the formatting of the timestamp can be prepared in advance.
- 5 000 updates a second: about 0.2% of a core with the indexed decoder and 4.9% with the Python reference.
- Missed updates: a first identifier above the last applied one plus one. The book is discarded and rebuilt from a new snapshot (Book 3’s
firm.wsbook). - So that a captured request cannot be replayed later: the venue refuses one older than its window.
- Named result. With a hop each way, decryption, framing, JSON and signing take 0.2% of the venue–host–venue path with the C++ components and 8.7% with the Python reference; reusing a connection saves about per REST order against opening one ( with resumption), several times the network path itself.
- When the strategy’s decisions are worth milliseconds, not microseconds, and messages are few: the network hop and the venue’s own latency dominate anyway.
- In a separate thread or process that terminates TLS for several connections, or in the operating system’s or the network card’s TLS support where available, so that the strategy’s thread sees plain frames.
- The same costs on the server’s processor (its AES and SHA instructions), under the real message rates and sizes, and the distribution of the network hop, not just its median.
- It is not processor time on this host, but it is most of the tick-to-trade and its variance; a closer placement changes the result more than any decoder.
- JSON and decimal text, and the masking; the parsing becomes fixed-offset reads (chapter 16).
- Feed the pool a scripted sequence of venue answers with 429 and a retry time, as Book 3’s governor accepts through
on_status, and check that it stops sending until the time has passed. - It keeps connections open, decodes without building anything and signs without rehashing the key.
17.10 Interview questions
Interview question 17.1 ★ developer
Why reuse HTTP connections for order entry? What does a new connection cost?
Solution
Solution of Interview question 17.1.
A new connection costs a TCP round trip, a TLS round trip and the handshake’s cryptography (milliseconds), and may count against the venue’s connection limits; a persistent connection pays this once. Use a pool behind the rate-limit governor.
What the interviewer is looking for: round trips plus handshake computation, and limits.
Interview question 17.2 ★★ developer
Describe a WebSocket frame. Why are client frames masked, and what does masking cost?
Solution
Solution of Interview question 17.2.
Two header bytes (final bit, opcode, mask bit, 7-bit length), an extended 16- or 64-bit length, a 4-byte masking key on client frames, the payload. Masking protects intercepting proxies from crafted payloads; it costs an XOR per byte, nanoseconds when done eight or thirty-two bytes at a time.
What the interviewer is looking for: layout, the reason, and the cost done well.
Interview question 17.3 ★★ developer
How would you parse a venue’s JSON depth updates as fast as possible?
Solution
Solution of Interview question 17.3.
Find all structural characters in one vectorised pass, then walk that index with the message’s expected shape, reading numbers in place and parsing prices into integers; fall back to a general parser if the shape differs. No tree, no allocation, no floating point.
What the interviewer is looking for: a structural index and a shape-specific walk.
Interview question 17.4 ★★ developer
How is an HMAC request signature computed, and how would you make signing cheap?
Solution
Solution of Interview question 17.4.
HMAC-SHA-256 of the query string and body with the secret key, sent in hex with the API key, a timestamp and a validity window in the payload. Hash the key’s two padded blocks once and copy those states per request; use a library SHA-256 that uses the processor’s instructions; prepare the fixed parts of the payload.
What the interviewer is looking for: the RFC 2104 precomputation, and replay protection.
Interview question 17.5 ★★ developer
What does TLS 1.3 session resumption save, and what does zero-round-trip data risk?
Solution
Solution of Interview question 17.5.
Resumption skips the certificate and part of the key exchange by using a key from an earlier connection: less computation, still one round trip. Zero-round-trip data is sent with the first message and can be replayed, so it must be idempotent, which an order is not.
What the interviewer is looking for: computation saved versus round trips, and replay.
Interview question 17.6 ★★★ developer, researcher
Your crypto strategy’s orders arrive 3 ms after the market data that triggered them, although the venue is 0.3 ms away. Where do you look?
Solution
Solution of Interview question 17.6.
Connection setup on the order path (a new connection or a reconnection per order), queueing behind a rate limiter or a busy connection, garbage collection or slow decoding in the client, TLS or signing done inefficiently, and the venue’s own latency; measure each step with timestamps at every boundary.
What the interviewer is looking for: measure the steps; handshakes first.