Venue Message Rate Limits: Architecting a Multi-Venue Quoting Engine Around Exchange Throttling

Order flow meeting three different exchange message-rate ceilings: parallel lines representing messages converge into a narrow aperture for CME Globex, Eurex T7 and Binance, each labelled with its enforcement unit, while rejected messages fold back toward the sender in red
Venue Message Rate Limits: Architecting a Multi-Venue Quoting Engine Around Exchange Throttling - cc394c08a87eda9cbd2bb5d52a72f8ed4f6b4449e2e293f9d15c0d26ccff2c0c?s=96&d=mm&r=g

Ariel Silahian

Ariel Silahian is a senior technology executive in institutional electronic trading, with 30+ years across the buy and sell side (New York, Miami, London, Hong Kong). He is the author of "C++ High Performance for Financial Systems" (Packt) and the creator of VisualHFT, the open-source microstructure analytics stack. He writes on exchange architecture, market microstructure, and execution quality, and advises a select number of trading firms on infrastructure decisions that move P&L. Book a strategy call at hftAdvisory.com

Most quoting engines carry one number for how fast they are allowed to send. It gets written into a config file early, usually by whoever last read a venue manual, and it stops being questioned. Then the engine is pointed at a second venue, and a third, and the number comes along unchanged.

That number is wrong on at least one of those venues, and probably on all but one, because the venues do not measure the same quantity. One counts messages inside a window. One measures a rate per second. One charges an amend to your account and another charges it to the address your packets leave from. The word “limit” is doing very different work in each case, and an engine that treats them as interchangeable will either leave capacity unused or breach at the exact moment its quotes matter most.

This piece walks through what the enforcement actually looks like across eleven venues, why the reject you receive is ambiguous in a way that matters, and how to structure the layer of the engine that has to hold the line. It is written for the person who has to sign off on the architecture, and for the desk that has to live inside it.

Contents

The unit is not consistent, even within one venue

Start with the venue that documents this most thoroughly, because it makes the point better than any comparison across venues could.

CME publishes its Messaging Controls openly. The page opens by telling you what is being measured: “Messaging Controls prevent violation of the maximum Transactions per Second (TPS) rule. Messaging Controls measure Transactions per Second (TPS) and are enforced at the iLink session level.” That reads unambiguously. The quantity is a rate.

Then, directly above the tables that carry the order thresholds, the same page describes the mechanism: “Messages submitted by the client system on an iLink session are monitored by the number of messages sent over a pre-defined time interval. The time interval begins with the first message processed. At the end of the interval, if no violation has occurred, the message count and interval reset pending the next message processed.”

Thank you for reading this post, don't forget to subscribe!

Subscribe by Email

That describes a counter. Messages accumulate inside an interval, the interval starts on the first message, and both reset if the interval closes clean. It is a fixed-window count, and the interval column in those tables reads “Three-second window.”

Then the very next sentence, still inside the section governing those same order thresholds, switches back: “If an iLink session exceeds one or more of the Reject thresholds, subsequent messages are rejected via a BusinessLevel Reject (tag 35-MsgType=j) message until the messages per second (MPS) rate falls below the threshold.” The recovery condition for a threshold described as a count is stated as a rate.

Further down, above the mass quote table, the page is explicit again and lands somewhere else: “All mass quote messaging is measured in messages per second (MPS) over a three-second window.” The administrative thresholds are stated the same way, as “an average of 100 administrative messages per second (MPS) over a three-second window.”

Now look at the threshold cells themselves. The mass quote row reads 400 MPS and 750 MPS, unit attached. The order rows read 500 and 750, and 1000 and 1500, with no unit at all.

Four separate message budgets on a single CME iLink session, showing that the units are not uniform: new order and cancel/replace reject at 500 and terminate at 750, cancel request and mass action reject at 1000 and terminate at 1500, both expressed as an unlabelled count inside a three-second window; mass quote and quote cancel reject at 400 MPS and log out at 750 MPS, expressed explicitly as messages per second; administrative messages reject at 100 and log out at 200 as a per-second average, with a sustained-equivalent column converting every row to the same unit

A count inside a three-second window and a rate per second differ by a factor of three, and the order thresholds are the ones left unlabelled. Read 500 as a count and the session sustains roughly 167 messages per second. Read it as a rate and the same session sustains 500, which is 1,500 messages across the same window.

The temptation is to resolve this by picking the reading you prefer. Do not. The two readings fail in opposite directions and both failures are expensive. Take the optimistic reading when the conservative one is correct and you have sized your cadence at three times your actual entitlement, so the engine breaches on the first sustained burst and the rejects arrive attributed to nothing your dashboard is watching. Take the conservative reading when the optimistic one is correct and you quote at a third of what you are paying for, then conclude you need another session or another cabinet to buy back capacity you already own.

Sustained messages per second for each CME budget once every row is converted to the same unit under the conservative count reading: mass quote sustains 400 per second, cancel and mass action sustains 333, new order and replace sustains 167, and administrative sustains 100, showing that the mass quote bucket prints the smallest trading threshold on the page at 400 and carries the largest sustained allowance of the three

There is a second-order consequence that is easy to miss. Under the count reading, the printed numbers rank the budgets in the opposite order to the real ones. Mass quote prints 400, the smallest of the three trading thresholds, and sustains 400 per second, the largest. New order and cancel/replace prints 500 and sustains around 167. An engine that treats the printed numbers as directly comparable will starve its quote stream to protect an order budget that was never the binding constraint.

The correct engineering response is not to choose. Take the conservative reading as the working default, because it is the one that cannot breach, and then measure. Walk the cadence up under controlled conditions until the first reject arrives, record the rate at which it did, and hold that measured value as session state. The documented number becomes a sanity check on the measurement rather than the other way round.

Two more details on this venue that belong in any implementation. Breaching the reject threshold returns a Business Reject, message type 35=j, carrying tag 380-BusinessRejectReason=8. Breaching the terminate threshold returns a Terminate carrying tag 39012-ErrorCodes=9. And CME’s New Release certification environment does not mirror production: it merges both order buckets into a single bucket at 20 reject and 70 terminate. A cadence that passes certification tells you very little about production, and a cadence tuned for production will fail certification.

Where the limit binds decides where your limiter lives

The threshold value is the part everyone reads. The scope is the part that decides your architecture.

CME meters at the iLink session. Eurex meters at the ETI session. Both of those are boundaries a single process can own, which means the limiter can be an object inside that process, holding its own counters, with no coordination problem to solve. Adding a second session adds a second independent budget, which is worth stating plainly: session count is a capacity decision, not only a resilience one.

Binance moves the boundary. An in-place amend on USD-M futures, PUT /fapi/v1/order, is charged against the account order-rate limits, X-MBX-ORDER-COUNT-10S and X-MBX-ORDER-COUNT-1M, and costs zero against the IP weight limit. Binance states the scope directly: “The order rate limit is counted against each account.” The spot equivalent, PUT /api/v3/order/amend/keepPriority, costs zero against the unfilled order count and instead consumes IP-based request weight, with a documented IP weight of 4.

Enforcement scope compared across three venues, showing which level of the firm, account, session and egress address hierarchy each venue actually meters: CME Globex meters at the iLink session, Eurex T7 meters at the ETI session with limits assigned by the venue, and Binance meters the account on futures and the egress IP on spot, meaning the budget escapes the process boundary entirely and a per-process limiter cannot enforce it

That difference has architectural consequences. A budget metered at the egress address escapes the process entirely. Every process behind that address shares one allowance, which means two independently correct limiters, each staying comfortably under the published ceiling, will jointly breach it. A per-process limiter is structurally unable to hold that line no matter how carefully it is tuned. The same applies to the account-scoped futures budget: two strategies on one account contend with each other even when they run on different hosts in different data centres.

Whatever the venue counts against, your allocator has to be shared across exactly that boundary. Session-scoped budgets permit a simple in-process design. Account-scoped and IP-scoped budgets require a shared allocator, and that is a materially harder piece of infrastructure with its own latency and failure characteristics.

The two Binance endpoints differ in a second way that is worth getting right, because it explains the first. The futures amend supports LIMIT orders only, and Binance states that “modified orders will be reordered in the match queue,” so queue priority is lost. The spot amend keeps it: “The amended order keeps its time priority among existing orders at the same price.” The reason spot can make that promise is that it is not the same operation. Spot’s endpoint reduces quantity only. It takes newQty, which “must be greater than 0 and less than the order’s quantity,” and carries no price parameter at all. Futures can reprice; spot cannot. Priority is preserved on the side that cannot move the price, which is exactly the trade being made.

The venue will sometimes tell you, at logon

Eurex takes a different approach, and it is the one more venues should copy.

The T7 Enhanced Trading Interface hands the session’s throttle parameters to the client in the Session Logon Response. Three fields carry the contract. ThrottleNoMsgs (1613) is the transaction limit per interval. ThrottleTimeInterval (1614) is that interval, in milliseconds. A third field at tag 25002 carries the number of sequential throttle rejects tolerated before the venue disconnects the session. The T7 R.13.0 manual gives that field as ThrottleRejectNoMsg (25002), while the OnixS field reference documents the same disconnect limit under the name ThrottleDisconnectLimit without printing the tag number, so implementations working from different sources will see different names for the same field. Take the tag number from the manual.

The manual’s own worked example sets ThrottleNoMsgs (1613) = 200 and ThrottleTimeInterval (1614) = 1000, which is 200 transactions per second expressed as 200 per 1,000 milliseconds. That is an illustrative value in the documentation, not a universal ceiling. The real one arrives on the wire, per session.

Eurex also names its algorithm, which almost no established derivatives venue does: “The transaction limit is the maximum number of messages that a participant application may send within a configured time interval without getting rejected (sliding window approach).”

Sequence diagram of the Eurex T7 ETI logon handshake showing the quoting engine sending a Logon and the venue gateway returning a LogonResponse that carries three throttle fields: ThrottleNoMsgs tag 1613 for the transaction limit per interval, ThrottleTimeInterval tag 1614 giving that interval in milliseconds, and tag 25002 giving the number of sequential rejects tolerated before disconnect, with a contrast between a config-driven engine holding a number written months earlier and a session-state engine that overwrites its limiter from these fields before the first order leaves

The architectural instruction follows directly. A config file holds a number somebody wrote months ago. The logon response holds the number that is true for this session, right now. An engine that reads the first and discards the second is choosing the stale value on purpose. The limiter has to be instantiated per session and configured from the handshake, because two sessions to the same venue can legitimately carry different ceilings.

The disconnect limit deserves its own treatment. It is a budget in its own right, denominated in consecutive rejects rather than messages. An engine that responds to a throttle reject by retrying immediately spends its disconnect allowance rather than its message allowance, and that one does not refill on the same terms.

Not every venue is this generous. The London Stock Exchange publishes the mechanism but not the number: a throttle breach returns a Business Message Reject on FIX, and a session is disconnected if it exceeds its rate “more than a configurable number of times, in any 30 second duration.” The ceiling itself is negotiated, since “the maximum throughput of each participant’s User/CompID will be agreed with London Stock Exchange.” ICE goes further and publishes nothing at all; the specification arrives through a support ticket. At the other extreme, Bybit returns your remaining budget on every single response, in X-Bapi-Limit, X-Bapi-Limit-Status and X-Bapi-Limit-Reset-Timestamp headers.

Discoverability is therefore a design input in its own right. Venues fall into three groups: those that tell you at runtime, those that publish a static number, and those where the number is negotiated or withheld. An engine that assumes it can always read the ceiling from documentation is fragile the moment it meets the third group.

A cross-venue taxonomy of rate-limit enforcement

Laid side by side, the enforcement models across eleven venues share almost nothing beyond the general idea that you can send too much.

Comparison table of message rate-limit enforcement across eleven trading venues, listing for each the unit of measurement, the scope it binds to, the underlying algorithm, the action taken on breach, and whether the ceiling is discoverable at runtime, covering CME Globex, Eurex and Xetra T7, Euronext Optiq, LSE Millennium, Cboe, Nasdaq Nordic, ICE, Coinbase, OKX, Bybit and Deribit

A few entries carry more design weight than the rest.

Euronext Optiq runs an explicit token bucket, with the bucket sized at 100 percent of the connection rate, and it starts full: “On every logon bucket is full, allowing them to immediately start sending messages.” That is a burst allowance handed to you at connection time, which is useful and also a trap if your own limiter does not model it. Throttle breaches arrive as a session-level reject carrying a specific reason: SessionRejectReason 26 for “Throttling Rate exceeded”, 25 for “Throttling queue full”, 27 for “System busy”. Excessive breaches disconnect the session with a three-second reconnect ban. Euronext documents the mechanism as serving MiFID II RTS 7, the standard covering venue resilience, which is a different instrument from the order-to-trade rules discussed later in this piece.

Cboe enforces something none of the others do: a hard cap on messages in flight. “The message in flight limit for US Equities FIX or BOE order entry sessions towards each matching engine is 32. The message in flight limit for US Options FIX or BOE order entry sessions towards each matching engine is 128.” When the cap is reached, the session sends nothing further until an acknowledgement returns. That is a credit window, operated by the venue.

Nasdaq’s Nordic and Baltic INET platform lowered its per-port ceiling from 20,000 to 10,000 messages per second per port, effective 26 May 2020. The number is worth reading carefully, because it was revised before it took effect: an April notice announced a drop to 5,000, and a second notice four days before the effective date replaced that with 10,000, promising a further reduction in a later notice that I could not find published. The notice also attaches a band to its own figure, stating the limit will be applied at that rate at the latest and may be applied at a rate up to 10 percent lower. A published ceiling is not always a fixed one. That number applies to that platform. I could not confirm an equivalent published figure for Nasdaq’s North American order entry protocols on any primary source, so this piece does not state one.

The pattern across the table that surprised me most is which venues name their algorithms. Coinbase publishes the refill formula for its REST limiter outright, describing it as a “lazy-fill token bucket” with token_amount = min(burst, previous_token_amount + (current_time - previous_request_time) * refresh_rate), and separately notes that “rate-limiting for FIX API uses a fixed window implementation” with the session disconnected above 200 messages per second. Deribit says its credit refill “acts like a leaky bucket.” Bybit states its limiter “is based on the rolling time window per second and UID.” The established derivatives venues describe behaviour, rejected until the rate falls below the threshold, and leave the mechanism to be inferred. The practical result is that a limiter built against a crypto venue can often be calibrated from the documentation, and one built against a traditional venue usually has to be calibrated by observation.

The algorithm zoo, and which one belongs on your side of the wire

Five algorithms cover essentially every enforcement model in the table above, and they have meaningfully different properties.

A fixed window counter keeps one count per interval and resets at the boundary. Constant memory, trivial to implement, and it carries a specific defect: up to twice the nominal rate can pass across a window edge, because a full allowance sent at the end of one window and another full allowance sent at the start of the next are arbitrarily close together in wall-clock time while remaining individually legal. CME’s application thresholds and Coinbase’s FIX limiter both work this way.

A sliding window log stores a timestamp per message and drops entries older than the window. It is the most accurate of the group and its memory grows with traffic, which rules it out at the message rates this article is concerned with.

A sliding window counter approximates the log by weighting the previous window’s count by the overlap fraction. Constant memory, bounded approximation error, no unbounded boundary burst. Eurex and Bybit both use this shape.

A token bucket refills at a fixed rate up to a maximum capacity, and each message consumes one token. Constant memory, and it permits a burst up to the full bucket instantly, which is a feature when you have sized the bucket deliberately and a problem when you have inherited it. Euronext and Coinbase REST are the documented cases.

A leaky bucket drains at a fixed rate regardless of the arrival pattern, smoothing bursts into a steady stream at the cost of queueing delay. Deribit’s credit system is the clearest example, and Kraken’s decaying REST call counter, which decays at 0.33, 0.5 or 1 point per second depending on tier and is a separate limiter from the one governing its order placement, is structurally the same idea.

For the client side I would reach for a sixth that none of these venues runs and that suits the job better: GCRA, the Generic Cell Rate Algorithm, also called virtual scheduling, standardised by the ATM Forum and ITU-T I.371 for traffic policing. It maintains a Theoretical Arrival Time and admits a message when its actual arrival satisfies ta > TAT - tau. It is exactly equivalent to a continuous-state leaky bucket with increment T and limit tau, capacity T + tau, and it is cheaper per message than a token bucket because there is no background refill process to run. It updates state only on arrival.

The property that matters here is subtler than the cost. GCRA has no window boundary. Every counter-and-reset scheme has a phase, and your phase is not the venue’s phase. Your window opens when your first message goes out; the venue’s opens on its own clock, and the two drift. A limiter that is exactly correct against its own window can present the venue with a pattern that is not correct against theirs. GCRA has nothing to misalign.

This is the first of two independent reasons a client-side limiter has to be strictly more conservative than the venue’s published ceiling. The second is that you cannot observe the venue’s counter. You can only infer its state from the rejects it sends you, which is feedback that arrives after the fact, on a delay, and only once you have already crossed a line. Sizing your limiter at exactly the published number assumes perfect knowledge of a quantity you can only estimate.

In-flight orders: why a fixed timer is the actual bug

Underneath the rate question sits a second quantity that most engines never instrument, and it is the one that actually causes the incident.

Little’s Law states that in a stationary queueing process, L = lambda * W: the mean number of units in the system equals the arrival rate multiplied by the mean time each unit spends there. Little proved it in 1961, in Operations Research, and the proof is distribution-free. That last property is what makes it safe here. You do not need to model the matching engine’s internal service-time distribution to use it.

Apply it to an order pipeline. lambda is your send rate. W is the round-trip acknowledgement time. L is the number of messages you have sent and not yet heard back about.

Little's Law applied to order flow, showing that in-flight messages equal send rate multiplied by round-trip acknowledgement time, with a table holding send rate fixed at 500 per second while acknowledgement time expands from 1 millisecond to 400 milliseconds and unacknowledged messages grow from 0.5 to 200, contrasted with two designs: a fixed timer that holds send rate constant so in-flight grows in proportion, and an acknowledgement-driven credit window that holds in-flight constant so send rate falls automatically

Hold the send rate fixed at 500 per second, which is what a timer-driven cadence does by construction, and let the acknowledgement time expand as the venue gets busy. At 1 millisecond you have half a message outstanding. At 10 milliseconds, five. At 100 milliseconds, fifty. At 400 milliseconds, two hundred.

Nothing inside the engine changed. Only the venue got slower. And now two hundred cancel/replace messages are outstanding, every one of which can land on an order that has already filled.

That is the whole argument for driving cadence from acknowledgements rather than from a clock. A fixed timer holds lambda constant and lets the venue move L. A credit window holds L constant, admitting a message only when a credit is free, which means that when W expands, lambda falls on its own. Backing off becomes the default behaviour of the design rather than something an exception handler has to notice and trigger.

Cboe removes the choice entirely by capping L at 32 or 128 per matching engine and holding the session until an acknowledgement returns. Against a venue like that the credit window is simply the enforcement model, and an engine built around a timer will stall against it without understanding why.

The failure sequence

The individual mechanisms above compose into one repeatable incident, and it does not require anything to be broken.

Six-step failure sequence showing how a healthy trading session throttles itself: volume spikes and required repricing rises, venue-side queuing grows and acknowledgement latency expands, the fixed timer does not notice so send rate stays flat, unacknowledged messages accumulate as in-flight equals send rate times acknowledgement time, a replace lands on an order that has already filled and is rejected leaving the local book stale, and the retry is metered too, consuming budget and on some venues the sequential-reject disconnect allowance

Volume spikes, and the strategy legitimately wants to reprice more often. The same spike is hitting the matching engine and everybody else connected to it, so acknowledgements slow down. The timer does not notice, because a clock has no input from the venue, so the send rate is unchanged. In-flight messages accumulate in exact proportion to the latency expansion. Somewhere in that population, a replace lands on an order that already filled, and the venue rejects a modification against an order that no longer exists in the state the engine assumed. The engine resends. That retry consumes budget like any other message, and on a venue counting sequential rejects it consumes the disconnect allowance instead.

Each step is individually reasonable. The composition produces rejects at the precise moment the desk most wanted its quotes in the market.

Worth separating clearly, because engines routinely conflate them: the too-late-to-cancel reject and the threshold reject are different events with different message types. CME delivers a too-late-to-cancel as an Execution Report, 35=8. It delivers a messaging threshold breach as a Business Reject, 35=j. In the generic FIX dictionary the same fill race surfaces as CxlRejReason(102)=0, “Too late to cancel”, on an Order Cancel Reject, 35=9. One is a state problem. The other is a rate problem. They demand opposite responses and they arrive together under load.

Four rejects that look alike and demand opposite responses

Most engines have a single reject handler. It usually retries. Retrying is the correct response to exactly one of the four things that arrive on that channel.

Four categories of reject that arrive on the same channel and require opposite responses: a throttle reject requiring immediate derating and never a retry because on some venues the retry spends the disconnect allowance, a state race where a cancel or replace hit an already-filled order requiring reconciliation rather than resending, a risk or credit reject requiring the engine to stop quoting that instrument and escalate, and an economic reject on price band or tick size requiring the order to be corrected and resent once

A throttle reject means slow down. Derate, widen the coalescing window, and do not retry, because on a venue that counts sequential rejects the retry is drawn from the disconnect budget. A state race, the cancel or replace that hit an order which already filled, means your view of the book is stale; reconcile against the venue’s view before acting, and do not resend the same replace, because the order is not there. A risk or credit reject is a business decision, not a transient; stop quoting that instrument and escalate to a person. An economic reject, a price band or tick size or lot size violation, means correct the order and resend once, remembering that the corrected resend is metered like anything else and a systematic error will resend forever.

Telling them apart is harder than it should be, because the venues do not agree on how to signal them.

Euronext sends throttle breaches as a session-level Reject(3) carrying SessionRejectReason 26. Eurex sends everything that way: “All rejections and errors on an application and session level are communicated via the FIX standard Reject (3) message.” The London Stock Exchange sends throttle breaches as a Business Message Reject, the opposite classification for a functionally identical event. Coinbase’s FIX interface does not send a throttle reject at all; it disconnects the session above 200 messages per second. CME uses Business Reject 35=j.

Meanwhile the FIX 4.2 specification itself says Reject(35=3) “indicates a serious error that may be the result of faulty logic in one or both applications.”

So the same message type means “you are going too fast, back off” at one venue and “something is structurally wrong with your session, stop and investigate” at another. A generic handler that maps Reject to backoff is wrong at some venue no matter which venue you wrote it against. The only reliable disambiguator is the venue’s own reason-code field, mapped per session adapter, which is precisely the work that tends to get deferred. In the interest of completeness: I was not able to confirm on a primary source the specific iLink 3 SBE reason code CME assigns to a messaging-control breach as distinct from general validation failures, so that mapping needs to come from CME directly rather than from this article.

A reference architecture for a venue-aware rate governor

Five layers, with a clear rule about which layer owns which state.

Strategy emits desired quote state and owns desired_quotes[], target_spread and size. It knows nothing about message budgets and should not, because rate awareness at this layer couples pricing logic directly to venue plumbing, the coupling that makes a strategy hard to port to a new venue later.

Cadence controller turns desired state into an actual message plan, and owns pending_intent[instr] and a coalesce_interval. Its job is to collapse repeated repricings of the same instrument inside one interval into a single outbound message carrying the latest intent, rather than sending three messages in a row where the first two are already stale by the time they reach the wire.

Session governor, one instance per session, per message class, owns credits[class], inflight, ewma_ack_ms, and derate_factor. This is the meter: it issues credits, tracks in-flight count, applies the control law described in the next section, and is the only layer in the stack permitted to decide that a message does not go out right now.

Session adapter owns venue_limits, populated from the logon response where the venue supplies one, and the msg_type-to-class map: which venue budget a given message type actually draws on. This is where the CME distinction from earlier in this piece becomes operational: a cancel and a cancel/replace are one intent as far as the strategy is concerned, but two different budgets as far as CME’s own thresholds are concerned, and the adapter is the layer responsible for knowing that.

Transport is the wire itself. It returns acknowledgements, rejects and disconnects upward, which is what closes the loop back into the session governor and lets the control law react to real, measured venue behaviour rather than an assumption made at startup.

Five-layer reference architecture for a venue-aware rate governor: strategy emits desired quote state with no knowledge of message budgets, a cadence controller coalesces repeated repricings into a single outbound message per interval, a session governor instantiated once per session and per message class owns credits, in-flight count and the control law and is the only layer permitted to withhold a message, a session adapter owns the venue's published limits and the map from message type to budget class, and transport returns acknowledgements, rejects and disconnects that close the loop back into the session governor

Venue-specific quantities live only in layers three and four by design. That containment is what lets one strategy run against venues whose enforcement units, scopes and algorithms are all different, the entire finding of the earlier sections of this piece, without the strategy itself carrying a single line of venue-aware logic.

The constraint that makes the rest of it true: the session governor has to be the only path to the wire. Every bypass gets written for what looks, at the time, like a good reason: a manual cancel-all button, a risk-driven kill switch, a reconnect replay that resends whatever was pending before a disconnect. Each of those paths tends to get built outside the governor, because it feels like an emergency measure rather than ordinary traffic, and each one fires precisely when the message budget is already under the most pressure. An unmetered mass cancel fired during a risk event is a common, specific way to convert that risk event into a session termination, at the exact moment continuity matters most. Every one of those paths belongs inside layer three, subject to the same credits and the same control law as ordinary quote traffic, not routed around it.

The control law

The session governor needs a rule for two decisions: how fast to send when the session is healthy, and how hard to pull back the moment it is not. Additive increase, multiplicative decrease answers both, and it is not a rule invented for trading sessions. Chiu and Jain proved the general result in 1989: “a simple additive increase and multiplicative decrease algorithm satisfies the sufficient conditions for convergence to an efficient and fair state regardless of the starting state of the network.” The same algorithm, applied to the same category of problem, a shared resource with delayed feedback about how much of it remains, is the direct ancestor of TCP congestion control, which is why the shape of the state machine below will look familiar to anyone who has read a networking textbook.

Five states. Normal: send at the planned cadence; in-flight is below target and credits are available for the class. Watch: the acknowledgement-latency EWMA has moved above its baseline, or in-flight has crossed 60 percent of target; stop increasing, but do not yet pull back. Derate: an actual throttle reject arrived; multiply the rate by 0.5 on each one, and widen the coalescing window at the cadence-controller layer at the same time. Hold: sequential rejects are approaching the venue’s disconnect limit; cancels only, no new quotes, until the session is measurably clean again. Recover: one clean interval at a time, rate moves toward the ceiling additively, never in a single jump back to full speed, specifically so a session that just breached does not immediately re-breach on the first message after recovering.

State machine for the session governor's control law: Normal transitions to Watch when acknowledgement latency rises above baseline or in-flight crosses 60 percent of target, Watch transitions to Derate on an actual throttle reject where rate is multiplied by 0.5, Derate transitions to Hold as sequential rejects approach the venue's disconnect limit and only cancels are permitted, and Hold and Derate both transition through Recover, which restores rate additively one clean interval at a time, back to Normal, implementing additive increase and multiplicative decrease

Four lines implement the state machine’s arithmetic:

on_ack:              inflight -= 1; ewma_ack = a*rtt + (1-a)*ewma_ack
on_throttle_reject:  rate = max(rate_floor, rate * 0.5)
on_clean_interval:   rate = min(rate_ceiling, rate + delta)
gate:                send iff (inflight < credit_target) and (credits[class] > 0)

Five quantities decide how the governor actually behaves, and none of them should be guessed. Each one has a derivation.

credit_target, the in-flight ceiling. Measure the session at nominal load rather than reading the documented ceiling. Take the sustained send rate and the p99 acknowledgement latency, multiply them for the in-flight count at nominal, then size for the tail rather than the median, because the tail is when this matters. A session sustaining 400 messages per second at a p99 acknowledgement latency of 25 milliseconds carries 10 messages in flight at nominal. Sizing at twice that gives a credit_target of 20, which also sits comfortably under the 32 that Cboe enforces directly on US equities sessions. If your measured number lands above a venue-enforced in-flight cap, the venue has already made this decision for you and your target has to sit below theirs.

delta, the additive increase step. Fix it from how long a full recovery should take. One multiplicative decrease halves the rate, so recovery has to climb back across half the ceiling. Choose the number of measurement windows that climb should occupy, at least eight, then set delta to half the ceiling divided by that count. Against a 400 per second ceiling recovering over ten three-second windows, delta is 20 messages per second per window, and a full recovery takes about thirty seconds. Recovering faster than that means the venue never observes that you backed off.

rate_floor, the derate floor. Derive it from risk, not from throughput. Ask how long you are willing to take to flatten the book, then divide your worst-case resting order count by that time. A desk holding 300 resting orders that wants to be flat inside two seconds needs a floor of at least 150 messages per second. On a venue that meters cancels against a separate and larger budget than replaces, express the floor against the cancel budget specifically, which is the one you will be drawing on in that state.

a, the EWMA weight on acknowledgement latency. Pick the smoothing time constant first and derive the weight from your sampling interval, a = 1 - exp(-dt / tau). A five second time constant sampled every 100 milliseconds gives a weight near 0.02. Too large and the EWMA tracks individual outliers into a derate; too small and it will not notice a real latency expansion until the rejects have already started.

clean_interval, the recovery trigger, needs an operational definition or the Recover state never fires reliably. Two conditions, both required, across one full venue measurement window: zero throttle rejects, and in-flight below credit_target for the entire window rather than on average. The window has to be the venue’s, three seconds on CME, not one you picked.

One structural note on credits[class]. Key it by the venue’s own budget classes, not by your internal message types. On a CME session that is four keys, matching the venue’s grouping, plus administrative traffic. Keying it by your own message taxonomy is how a cancel/replace ends up drawing on the wrong counter.

The second ceiling: order-to-trade ratio and messaging efficiency

Everything up to this point measures a ceiling with a time constant of one to three seconds, enforced in the message path, with feedback that is immediate and machine-readable: a reject, on the session that caused it. A second ceiling sits underneath it, measured over a trading day to a calendar month, enforced nowhere in the message path at all and surfacing only afterward, as an invoice or a disciplinary notice, weeks later.

The legal root, in Europe, is Commission Delegated Regulation (EU) 2017/566, Article 3, commonly referred to by the shorthand RTS 9 even though that string does not appear in the regulation’s own text. It defines the order-to-trade ratio two ways, by volume and by count: “(total volume of orders/total volume of transactions) – 1” and “(total number of orders/total number of transactions) – 1.” Article 1(a) carves out three categories of cancellation from counting against you at all: cancellations sent after “uncrossing in an auction,” after “a loss of venue connectivity,” or through “the use of a kill functionality.” The mass-cancel path built into the Hold state of the control law above, the one that fires during a risk event, is specifically exempted from this ratio by the same regulation that created it, even while that same mass cancel remains fully metered by the technical throttle discussed in every earlier section. The two ceilings do not share an exemption list.

Germany’s implementing statute, the Securities Trading Act, section 26a, sets the measurement window explicitly, per BaFin’s own English translation: “The order-to-trade ratio shall be determined for each financial instrument on the basis of the numerical volume of the respective orders and transactions over the period of one month.” Eurex’s own Excessive System Usage Fee, in its December 2025 concept paper, runs on a shorter but still non-real-time cycle: calculated “per Participant, per product, per trading day, and per limit type,” with fewer than four violations in a month treated as accidental and free, and violations from the fourth on treated as systematic and charged retroactively for the entire month. The fee scales in tiers: 0.05 euro per transaction for up to 50 percent over the limit, 0.10 euro per transaction from 50 to 100 percent over, 0.25 euro per transaction beyond that. The paper’s own worked example, 1,500,000 transactions against a calculated limit of 580,000, on a single product, for a single trading day, totals 128,500 euro. CME’s Messaging Efficiency Program runs a comparable escalation on the futures side: a Volume Ratio computed as messaging score against traded volume, benchmarked quarterly per product group, escalating through a warning, a second warning, a $10,000 surcharge per subsequent occurrence, and finally iLink port closure carrying its own $1,000-per-day fee for as long as the port stays closed.

Comparison of two distinct rate ceilings governing the same order flow: a technical throttle measured in seconds and enforced in the message path with an immediate reject, against an economic order-to-trade ratio measured over a trading day to a calendar month that rejects nothing and instead accrues, illustrated with Eurex's worked example of a 128,500 euro Excessive System Usage Fee for one product on one trading day and CME's Messaging Efficiency Program escalating from a warning to a 10,000 dollar per-occurrence surcharge and a 1,000 dollar per day port closure fee

Two crypto venues run a comparable discipline with no regulator requiring it of them. Deribit publishes its own order-to-volume ratio, matching-engine changes divided by traded volume, and calls anything above 10,000 for BTC or 1,000 for ETH high; a single mass quote inserting 100 double-sided quotes adds 200 matching-engine changes to that ratio in one message, and an IOC or FOK order that gets cancelled counts double against it. OKX runs the relationship in the opposite direction: a better fill ratio earns a higher order-rate ceiling rather than a penalty for a worse one, the mechanism noted in the taxonomy section above, where above VIP5 the ceiling itself is recalculated from a trailing seven-day fill ratio. Neither venue answers to MiFID II. Both arrived at the same underlying judgment Europe legislated: a participant sending many orders per fill imposes a cost on the venue’s infrastructure that a participant sending few orders per fill does not, and both venues decided that cost is worth pricing.

The design consequence is the one an engine tuned only against the first ceiling will not see coming. A governor that meters messages per second, exactly the governor built across the preceding sections of this article, optimises against the technical throttle and is structurally blind to the economic ratio, because the economic ratio measures messages per execution, not messages per second, a different quantity entirely. An engine can move the two in opposite directions at once: derating the send rate improves the first ceiling immediately, while quote churn per fill, if it keeps climbing, worsens the second. Both quantities have to be instrumented, separately, and the slower-moving one has to be attributed at the granularity the actual bill arrives at: per venue, per product, per trading day. A dashboard that only plots messages per second will show a governor doing its job perfectly while the desk accrues a fee it will not see until the invoice, or the disciplinary notice, arrives.

What breaches look like in public

Four public records are worth reading in full, and they sit at different points on the severity scale.

The cleanest on-topic case is recent. CME disciplinary file CME-24-1779-BC, effective 31 October 2025, records that WH Trading LLC ran an automated system entering quotes with incrementally widening prices, and that “the activity consisted of 191 instances in which WH Trading messaged instrument quotes that exceeded 200 distinct prices in less than one second.” The panel ordered a $30,000 fine under Rules 432.Q and 432.W. The amount is small. What makes it instructive is that the conduct continued after Market Regulation flagged it, and that the firm had already reduced its own internal messaging threshold. A threshold the system can cross while continuing to send is a setting rather than a control.

Knight Capital remains the anchor case for scale. The SEC’s 2013 settlement records that Knight’s router “rapidly sent more than 4 million orders into the market when attempting to fill just 212 customer orders” in the first 45 minutes of trading, traded more than 397 million shares, paid a $12 million penalty in the first enforcement action under Rule 15c3-5, and lost more than $460 million. BaFin’s 2024 action against Citigroup Global Markets Europe AG carries a 12,975,000 euro fine over a May 2022 incident in which the firm “did not have appropriate systems and risk controls in place to ensure that its trading systems are subject to appropriate trading thresholds and limits.”

It is worth being precise about what those two cases are. Knight and Citigroup are broad pre-trade risk-control failures, not pure message-rate breaches, and using them as throttle cautionary tales overstates the case. They belong here because they show what the same missing control layer produces when the volume is large enough. WH Trading is the one that is squarely about message rate.

Cboe’s Regulatory Circular RC25-013, issued 5 November 2025, points at the other end of the same problem: it prohibits patterns of preventing messages from reaching the exchange gateway and of submitting non-actionable messages to reduce apparent latency. The direction of travel across all four is one way. This control surface is being tightened, not relaxed.

The two signals to plot

Two series, sampled finer than the venue’s own measurement window, will tell you which of these failure modes you are in. Plot the count of orders sent and not yet acknowledged, and plot how fast you are sending. Their divergence names the fault.

Two-by-two diagnostic matrix plotting in-flight unacknowledged orders against send rate: in-flight climbing while send rate stays flat means the venue is holding you and the correct response is to derate immediately, both climbing means the strategy is the source and messages per fill has risen so churn must be reduced rather than just send rate, both flat is nominal and the moment to capture the baseline against which derate thresholds are set, and send rate climbing while in-flight stays flat means headroom is being consumed while the venue absorbs it so remaining credit should be checked per message class

In-flight climbing while the send rate stays flat means the venue is holding you. Acknowledgement latency is expanding upstream of your limiter and your own cadence has not moved, because nothing told it to. Derate now; the ceiling is being enforced somewhere you do not control.

Both climbing means you are the source. Messages per fill has risen, which is a strategy parameter rather than a transport problem, and reducing the send rate alone will not fix it. This is also the quadrant that worsens the second ceiling described above.

Both flat is nominal, and it is the most valuable state to record rather than ignore. This is the baseline every other quadrant is measured against, and a derate threshold set without one is a guess.

Send rate climbing while in-flight stays flat means headroom is being consumed and the venue is absorbing it comfortably. It is healthy, and it is the quadrant most worth watching, because the next constraint you meet will be the message budget rather than the latency. Check remaining credit per message class, not in aggregate.

Both series have to be sampled finer than the venue’s measurement window. A one-second average cannot show you a burst that a three-second window is already penalising.

The practical framework

A checklist for a review of an existing engine. Each item is a question with a verifiable answer.

  1. Per session, per message class. Does the limiter instantiate once per session and per budget class, or is there one aggregate counter per venue? An aggregate counter cannot see that the quote budget is exhausted while the cancel budget sits idle.
  2. Unit stated explicitly. For every threshold in your config, is it recorded as a count per window or a rate per second, with the source and the date it was read? Any threshold whose unit is not written down is a threshold nobody can check.
  3. Ceiling read from the session. Where the venue supplies limits at logon, does the engine overwrite its configured values from the handshake before the first order leaves, or does it discard them?
  4. Scope matches the venue’s scope. For account-scoped and IP-scoped budgets, is the allocator shared across every process inside that boundary? If two processes can each pass their own check, the check is not the control.
  5. Cadence driven by acknowledgements. Is there a credit window on in-flight messages, or does a timer set the rate? Plot L under a stress window and see whether it is bounded.
  6. One handler per reject type. Are throttle, state race, risk and economic rejects handled separately, with the venue’s own reason codes mapped per adapter? Read the code path and count the branches.
  7. No path bypasses the governor. Search the codebase for every call site that writes to the session. Cancel-all, kill switch and reconnect replay are the three that are usually outside it.
  8. Both ceilings instrumented. Is messages per execution measured, per venue and per product, alongside messages per second? The first is what the invoice is computed from.
  9. Measure the ceiling yourself. Has anyone walked the cadence up under controlled conditions and recorded the rate at the first reject? Compare that number against the documented one, per class.
  10. Certification thresholds differ from production. Where the venue runs different thresholds in its test environment, is that difference recorded, so a passing conformance run is not mistaken for evidence about production?

Where this is still unsolved

One part of this I have not closed, and I would rather say so than present a clean answer that does not exist.

When a venue meters at the egress address, as Binance does for spot request weight, the budget belongs to a boundary that has no natural owner in the system. Every process behind that address draws on one allowance, and those processes frequently have no coordination channel, no shared clock worth trusting at this resolution, and different lifecycles. The same problem appears one level up with account-scoped budgets, where two strategies contend even from different hosts.

Three designs are available, and each fails somewhere specific.

A central allocator holds the budget and hands out credits on request. It is correct, and it puts a synchronous network round trip in the send path, which is the last place anyone wants one. The cost is manageable if processes lease in blocks rather than per message, say fifty credits at a time, which amortises the round trip across fifty sends. The residual problem is what happens when the allocator is unreachable, and the only safe answer is that every process falls back to a static floor small enough that all of them together stay under the ceiling, which is the static partition below with extra steps.

A static partition divides the budget by process up front. No dependency, no round trip, trivially correct. It wastes whatever the quiet processes do not use, and the waste is worst exactly when it hurts most: during a burst, when one strategy needs far more than its share and the others are sitting on allowance they are not using.

Epoch leasing sits between them. Each process takes a share for a fixed epoch, unused share returns to a pool at the epoch boundary, and the next epoch redistributes on observed demand. It recovers much of the waste without a per-message dependency. It also introduces a new phase problem of exactly the kind described earlier in this article: your epoch boundary is not the venue’s measurement boundary, and a process that receives a large share right before a venue window closes can present the venue with a burst that no individual process considers illegal.

What I have not closed is the case underneath all three. A burst that matters is shorter than any coordination round trip, so by the time an allocator has learned that one process needs more, the moment it needed it has passed. Every scheme above is adaptive on a timescale longer than the event it is adapting to. I do not have a scheme that is both cheap enough to sit in the send path and adaptive inside a single burst, and I would want to see one demonstrated across a real volatility window rather than a benchmark before believing it.

Here is the measurement worth running against your own stack, because it is falsifiable and takes an afternoon. Pick one venue and one message class. Walk the cadence up under controlled conditions until the first reject arrives, and record the rate at which it did. Then compare that measured ceiling against the documented one, for that specific class. If the two agree, you have confirmed your reading of the unit. If the measured ceiling comes in near a third of the documented number, or near three times it, you have found a unit mismatch, and it has been in your config the whole time.

The number that comes back, and the venue and message class it came from, is the thing worth bringing to your next architecture review.


Beyond this article, I advise a small number of trading firms on the infrastructure decisions that commit capital: execution quality, latency, and build versus buy. If that is useful to you, a strategy call can be booked at hftAdvisory.com.


This article expands on a LinkedIn post.

Never Miss an Update

Get notified when we publish new analysis on HFT, market microstructure, and electronic trading infrastructure. No spam.

Subscribe by Email

Ariel Silahian is a senior technology executive in institutional electronic trading, with 30+ years across the buy and sell side (New York, Miami, London, Hong Kong). He is the author of "C++ High Performance for Financial Systems" (Packt) and the creator of VisualHFT, the open-source microstructure analytics stack. He writes on exchange architecture, market microstructure, and execution quality, and advises a select number of trading firms on infrastructure decisions that move P&L. Book a strategy call at hftAdvisory.com

Leave a Reply

Your email address will not be published. Required fields are marked *