An order arrives at the gateway. Before it goes out to the venue, something has to decide whether the account is allowed to send it. That decision runs on every order, including the overwhelming majority that are perfectly fine, so its cost is paid by all normal traffic and not only by the rejects.
That is the constraint to name first. A risk check placed in the order path is a tax on order entry. The engineering question is not how to make the check clever, it is how to make it small enough that traders do not feel it, and honest enough that it still refuses the orders it must refuse.
The short answer first. The checks that belong in the order path are position and exposure limits, margin, fat-finger bounds, instrument and account state, the kill switch, and duplicate guards. On the risk path amBrain builds for brokers and prop firms, the pre-trade decision on in-memory state completes in under 1 ms - a figure that covers the check itself, not the end-to-end journey of an order. The rest of this article is how that path is built and where it stops.
What actually belongs in the order path
The hot path answers exactly one question: may this order be sent right now, given what we currently know about this account. Checks that answer that question stay in. Checks that answer a different question move out.
- Position and exposure limits - the resulting position in the instrument, the group and the account, compared against configured bounds
- Margin or buying power - whether the account still has room for the order under the current margin model
- Fat-finger bounds - order size, notional and price distance from a reference, catching the typo before the venue does
- Instrument and account state - trading halted, account restricted, close-only, product not enabled for this account
- Kill switch state - a single flag that overrides everything above
- Duplicate and self-trade guards where the venue does not provide them
Everything else runs beside the path, on the same state, without holding the order. It informs the limits that the hot path enforces, but it does not sit between the trader and the venue.
- Portfolio risk analytics - scenario runs, stress tests, correlated exposure across accounts
- Margin model re-rating when parameters change, and any recalculation that touches the whole book
- Surveillance and pattern detection, which needs history the hot path deliberately does not carry
- Reporting, reconciliation and anything that talks to a database or an external service
- Credit and counterparty review, which operates on a slower clock by nature
The dividing line is a question, not a category. In the path: may this order go out. Beside the path: what should the limits be. Anything that answers the second question and still blocks the order is a design mistake, however important the check is.
Where position state lives, and why the database is not on the path
The state a pre-trade check needs - current positions, working orders, used and available margin, limit configuration - lives in the memory of the process that makes the decision. Not in a cache in front of a database, not behind a network call. In the process.
The reason is not only speed, though a query is orders of magnitude more expensive than a lookup in a local array. The reason is correctness. A database holds the position as it was written. The risk check needs the position including orders sent a moment ago that have not been filled, acknowledged or persisted yet. If you read from storage, you check against a past that has already been overtaken by your own flow.
Practically, that shapes the process the way any low-latency component gets shaped:
- One writer per account. Accounts are sharded across risk instances so that an account's state is never contended, and no lock is taken on the order path
- Flat, pre-allocated structures - fixed-size arrays indexed by account and instrument id, resolved at session start, not hash lookups on strings built per order
- No allocation, no I/O and no logging that blocks on the decision path; the audit record is handed to another thread through a queue
- Configuration that changes without a restart is swapped in as a whole immutable snapshot, so the check never reads a half-updated limit
The database is where the position is recorded. It is not where the position is known.
Incremental recalculation, not a full pass
A full recomputation of an account's exposure and margin walks every position and every working order. That cost grows with the size of the book, which means the risk check would get slower for exactly the clients who trade the most. So the hot path does not recompute. It applies a delta.
The account carries running aggregates - net and gross exposure per instrument and per group, used margin, notional in flight. An incoming order produces a small change to those aggregates, the changed values are compared against the limits, and the order is accepted or rejected. The work is proportional to the order, not to the portfolio.
- On send, the order's worst-case effect is reserved against the aggregates, so two orders in flight cannot both fit into the same remaining headroom
- On reject, cancel or expiry, the reservation is released; on fill, the reservation is replaced by the realised position change
- Partial fills adjust both sides of that in one step, which is where most bugs in this kind of engine actually live
- Netting and grouping rules are resolved when the instrument is loaded, not per order, so the delta is a handful of arithmetic operations
Full recomputation still happens - on a schedule, when margin parameters change, and as a periodic self-check against the incremental result. It runs off the path, on a copy, and its result is either swapped in or raised as a discrepancy. Incremental state that silently drifts from the true state is worse than no check at all, so the comparison is not optional.
Measured on the risk path we build, the pre-trade check itself completes in <1 ms. That figure covers the decision on in-memory state, not the full journey of an order from the client to the venue and back.
The kill switch is a separate path
A kill switch is used precisely when something is already wrong. That rules out building it on top of the machinery that may itself be the thing that is wrong. It is a separate path, with its own rules.
- It is a single atomic flag read at the top of the check, before position state, margin or instrument data is touched - so it works even when those are stale, missing or broken
- It is set from several independent triggers: an operator action, an automated condition, a loss of the market data or fill feed the risk state depends on
- It fails closed. If the risk process cannot establish that it has valid state, the gateway behaves as if the switch is engaged
- It has scopes - the whole firm, one desk, one account, one strategy - because a switch that can only stop everything gets used too late
- Engaging it is one action and one confirmation, not a configuration deploy; disengaging it is deliberate and always recorded
Stopping new orders is the easy half. The harder half is what the switch does to orders already resting at the venue: pulling quotes and cancelling working orders has to be possible while the sending path is disabled. That cancel path deserves its own testing, because it is exercised on the worst day rather than on a normal one.
Restart, and how state comes back
In-memory state is a derived view of a durable record. That is what makes a restart survivable. Every event that changes risk state - an order accepted, a reservation released, a fill applied, a limit changed, the switch engaged - is appended to a journal on the local machine before it is acted upon downstream.
- On start, the process replays the journal to rebuild aggregates, then reconciles against the venue and clearing drop copy for positions and working orders
- Until reconciliation completes, the account is not open for trading. A risk engine that accepts orders while it is still figuring out the position is not a risk engine
- A mismatch between the replayed state and the venue's view stops that account and raises an alert; it is never resolved by quietly preferring one side
- A hot standby follows the same journal, so a failover restores a warm state instead of a cold replay, and the standby is verified by being promoted regularly rather than in theory
Recovery time then depends on journal length and drop copy availability, not on the size of the book, and the failure mode of every unknown is the same: refuse to trade the account.
What this design does not give you
The honest limits are worth stating plainly, because they decide whether this architecture fits at all:
- The check is only as correct as the fill feed. If drop copy or execution reports lag, exposure is understated, and the correct response is to degrade to conservative limits or engage the switch rather than to keep trading on stale state
- Portfolio margin models that are genuinely non-additive resist incremental evaluation. What works is a conservative incremental bound on the path plus a full model off the path; the price is that some orders are rejected which a full model would have allowed
- The switch protects against your own flow, not against the market. It cannot prevent a gap or a slippage on positions you already hold
- In-process state means the risk engine and the order gateway share a fate. That buys latency and costs you the ability to scale them independently
- Single-writer sharding by account makes cross-account limits harder, and firm-wide checks need a slower aggregation layer with its own staleness
- It is more operational work than a database-backed check: journals, reconciliation, standby promotion drills. If order entry is not latency-sensitive, this complexity is not worth buying
amBrain builds this kind of pre-trade risk path for brokers and prop firms, with the hot paths written in Rust. The team has worked on trading infrastructure from Yerevan, Armenia since 2019.
Originally published at ambrain.org.