Matching Engine Architecture

Your exchange processed 14 million orders yesterday without incident. Today a market maker submits a batch of 50,000 cancel-replace messages in 200 milliseconds. Your matching engine, which parallelizes order processing across 8 threads for throughput, produces a nondeterministic fill sequence. Two traders submit identical limit orders at the same price within the same microsecond. Depending on which thread wins the race, trader A gets filled first on one replay and trader B gets filled first on another. Your audit log says one thing. Your risk system, replaying the same sequence on a different core, says another. The exchange has lost determinism, and with it, the ability to prove that price-time priority was honored.

The matching engine is the only component in a trading system where single-threaded execution is not a performance limitation — it is a correctness requirement.

Why Single-Threaded

A matching engine’s job is to accept orders, apply matching rules, and emit fills. The order matters. Price-time priority — the foundational fairness guarantee of most exchanges — requires that orders at the same price are filled in the order they arrived. If two threads process orders concurrently, arrival order becomes ambiguous. The moment you parallelize, you introduce a race condition into the fairness model.

LMAX Exchange demonstrated this with their Disruptor architecture: a single-threaded business logic processor handling 6 million orders per second on commodity hardware. No locks, no contention, no context switches. The entire order book lives in L1/L2 cache. The single thread processes each message in sequence, and the sequence is the source of truth.

Architecture Throughput Deterministic Audit-safe
Multi-threaded with locks Variable (lock contention) No No
Sharded by symbol N × single-thread Per-shard yes Yes (per shard)
Single-threaded event loop 6M+ orders/sec Yes Yes

The sharded model — one thread per symbol or symbol group — is a common compromise. Each shard is internally deterministic. Cross-symbol operations (portfolio margin, cross-asset spreads) require coordination between shards, which reintroduces ordering complexity at the boundary.

The Event Loop

The matching engine runs a tight loop: read a message from the input queue, process it, write the results to the output queue. No I/O, no system calls, no allocations in the hot path.

loop {
    message = input_queue.read()      // memory-mapped ring buffer
    match message.type {
        NewOrder    => process_new_order(message),
        Cancel      => process_cancel(message),
        Replace     => process_replace(message),
    }
    output_queue.write(results)       // fills, acks, rejects
    sequence_number += 1
}

The input queue is a pre-allocated ring buffer in shared memory. The gateway process writes inbound messages to the ring buffer. The matching engine reads from it. No system call, no kernel transition — both processes map the same memory region. LMAX’s Disruptor uses this pattern with mechanical sympathy: the ring buffer is sized to fit in L3 cache, and entries are padded to cache-line boundaries (64 bytes) to prevent false sharing.

The output side mirrors the input. Fills, acknowledgments, and rejects are written to an output ring buffer. Downstream consumers — risk engines, market data disseminators, drop-copy feeds — read from it independently.

Any system call in the hot path — malloc, write, futex — adds 1–10 microseconds of latency. At 6 million messages per second, each message has a budget of 167 nanoseconds. A single malloc can consume the entire budget.

Order Book Data Structures

The order book is a sorted collection of price levels, each containing a queue of orders at that price. The two critical operations are insertion (new order at a price) and matching (walk the opposite side of the book, filling orders in price-time priority).

Price levels are typically stored in a red-black tree or a sorted array. A red-black tree provides O(log n) insertion and lookup, where n is the number of distinct price levels. For liquid markets with 50–200 active price levels, a red-black tree lookup completes in 6–8 comparisons. A sorted array with binary search has similar asymptotic cost but better cache locality — the entire price ladder fits in a few cache lines.

Orders within a price level are stored in a FIFO queue — a doubly-linked list for O(1) insertion at the tail and O(1) removal of any node (cancellation). The order’s node is indexed by order ID in a hash map for O(1) cancel lookup.

struct OrderBook {
    bids: BTreeMap<Price, OrderQueue>,    // descending by price
    asks: BTreeMap<Price, OrderQueue>,    // ascending by price
    order_index: HashMap<OrderId, *mut OrderNode>,
}

struct OrderQueue {
    head: *mut OrderNode,
    tail: *mut OrderNode,
    total_quantity: u64,
}

A cancel operation looks up the order by ID in the hash map (O(1)), unlinks it from the doubly-linked list (O(1)), and updates the queue’s total quantity. No tree traversal, no search. On CME’s Globex platform, cancel rates reach 95% of total message volume during active trading — the cancel path must be as fast as the insert path.

Operation Complexity Typical latency
New order (no match) O(log p) tree insert + O(1) queue append 200–500ns
New order (match) O(log p) lookup + O(k) fills 300ns–2µs
Cancel O(1) hash lookup + O(1) unlink 100–300ns
Replace O(1) cancel + O(log p) insert 300–800ns

Where p is the number of price levels and k is the number of fills generated.

Matching Semantics

Price-time priority is the most common matching algorithm: the best-priced order fills first, and among orders at the same price, the one that arrived first fills first. The matching engine walks the resting book in price priority, consuming liquidity at each level until the aggressing order is fully filled or the book is exhausted.

fn match_order(book: &mut OrderBook, order: &mut Order) -> Vec<Fill> {
    let mut fills = Vec::new();
    let opposite_side = match order.side {
        Buy  => &mut book.asks,  // buyer matches against asks
        Sell => &mut book.bids,  // seller matches against bids
    };

    while order.remaining > 0 {
        let best_level = match opposite_side.first_entry() {
            Some(level) => level,
            None => break,  // book exhausted
        };
        if !order.price_crosses(best_level.key()) {
            break;  // no match at this price
        }
        // fill against resting orders in FIFO sequence
        while order.remaining > 0 && !best_level.is_empty() {
            let resting = best_level.front();
            let fill_qty = min(order.remaining, resting.remaining);
            fills.push(Fill { aggressor: order.id, resting: resting.id,
                              price: *best_level.key(), quantity: fill_qty });
            order.remaining -= fill_qty;
            resting.remaining -= fill_qty;
            if resting.remaining == 0 { best_level.pop_front(); }
        }
        if best_level.is_empty() { opposite_side.pop_first(); }
    }
    fills
}

Pro-rata matching — used for some futures contracts — allocates fills proportionally to order size rather than arrival time. This changes the data structure requirements: the engine must iterate all orders at the best price level and allocate based on each order’s share of total quantity. Pro-rata incentivizes large resting orders over fast ones, which changes market microstructure.

Algorithm Priority Incentivizes Used by
Price-time (FIFO) Price, then arrival time Speed Most equity/crypto exchanges
Pro-rata Price, then order size Liquidity provision CME Eurodollar, some options
Price-time with allocation Price, time, then pro-rata for remainder Both ICE, some hybrid venues

Event Sourcing and the Sequence

Every message into the matching engine and every result out of it receives a monotonically increasing sequence number. The sequence is the canonical ordering. The order book at any point in time is a deterministic function of the message sequence from the beginning.

This is event sourcing applied to the most latency-sensitive system in finance. The event log is the source of truth. The in-memory order book is a projection — a materialized view that can be rebuilt by replaying the log from any checkpoint.

The sequence enables three critical capabilities:

Deterministic replay. Feed the same input sequence to a fresh matching engine instance and it produces identical output. This is how exchanges validate their audit trail, test new matching logic, and prove to regulators that price-time priority was honored for every fill.

Gap detection. Every consumer of the output stream checks for sequence gaps. A missing sequence number means a missed message. Market data feeds, risk engines, and drop-copy systems all validate sequence continuity. A gap triggers an immediate resynchronization — the consumer requests a snapshot of the current book state plus the output stream from the gap forward.

Consistent snapshots. A snapshot is the order book state at a specific sequence number. Snapshot S plus all messages after sequence S reconstructs the current book. Snapshots are taken periodically (every 60 seconds or every N messages) and stored for recovery.

Deterministic replay is not optional for a regulated exchange. MiFID II (EU), Reg SCI (US), and similar frameworks require that exchanges can reconstruct the exact state of the order book at any point in time, including every intermediate state between fills.

Failover and Recovery

The matching engine runs as a hot-standby pair. The primary processes messages and writes to the output stream. The standby replays the same input stream, maintaining an identical in-memory order book, but does not write to the output stream. If the primary fails, the standby promotes itself and begins writing output.

Recovery time is bounded by the gap between the last snapshot and the current sequence number. With snapshots every 60 seconds and a throughput of 100,000 messages per second, the standby replays at most 6 million messages on failover. At replay speeds of 10–50 million messages per second (no I/O, no network, pure computation), recovery completes in 120–600 milliseconds.

Snapshot interval Max replay on failover Recovery time (at 20M msg/sec replay)
10 seconds 1M messages 50ms
60 seconds 6M messages 300ms
5 minutes 30M messages 1.5 seconds

The standby validates its state against the primary’s output. If the standby produces a different fill for the same input sequence, it halts and raises an alert — the divergence indicates a bug in the matching logic or a corrupted input stream.

Some exchanges run a third instance in a different data center for disaster recovery. This instance replays asynchronously with a deliberate lag (seconds to minutes), providing a recovery point that survives a full site failure.

The Latency Budget

Matching engine latency is measured at three points: wire-to-wire (network ingress to network egress), gate-to-gate (gateway ingress to gateway egress), and engine-only (message read to result write). The engine-only number is what the matching logic controls.

Exchange tier Engine-only latency Wire-to-wire latency
HFT-focused equity (IEX, MEMX) 1–10µs 20–50µs
Major futures (CME Globex) 1–5µs 10–30µs
Crypto (Binance, Coinbase) 5–50µs 100µs–1ms
Startup exchange 50–500µs 1–10ms

The gap between engine-only and wire-to-wire is network serialization, gateway processing, and kernel overhead. Kernel bypass (DPDK, Solarflare OpenOnload) eliminates the kernel’s network stack, saving 5–15 microseconds per message. FPGA-based gateways move serialization and basic validation into hardware, saving another 2–5 microseconds.

Measurement discipline matters more than raw speed. A matching engine that reports median latency of 5 microseconds but has a p99.9 of 500 microseconds has a jitter problem. The outlier — caused by a garbage collection pause, a cache miss on a cold price level, or an OS interrupt — is what market makers price into their quotes. Reducing jitter (tightening the gap between median and tail) often matters more than reducing median latency.

The standard measurement tool is a hardware timestamping NIC (Solarflare, Mellanox) that stamps packets at the network interface with nanosecond precision. Software timestamps using clock_gettime(CLOCK_MONOTONIC) have 25–100 nanoseconds of jitter from the system call itself — acceptable for microsecond-resolution measurement but not for nanosecond-resolution analysis of the matching engine’s inner loop.