Introduction

Every time you click Buy on a stock, crypto, or futures exchange, a tiny algorithm decides whether your order fills immediately, waits in a queue, or goes unfilled. That algorithm is called a matching engine, and its data structure is the limit order book.

The book is a sorted list of resting orders split into two sides. The bid side collects buyers willing to pay up to some price; the ask side collects sellers demanding at least some price. The highest bid and the lowest ask sit face to face — and the gap between them is the spread, the cost of trading.

When a new order arrives, the matching engine scans the opposite side of the book in price-time priority: best price first, and among equal prices, whoever arrived earliest wins. It is a deterministic, sub-microsecond arbitrator that never argues and never sleeps — the closest thing finance has to a physical law.

Try the Matching Engine

Submit limit orders on both sides and watch the book fill up. When bids and asks overlap, the matching engine fires and fills the trade at the resting order's price. A market order sweeps the entire opposite side until it is filled or the book runs dry.

<!-- Limit order book demo — submit limit and market orders, watch price-time matching. -->
<div class="controls">
  <div class="row">
    <label>Side</label>
    <div class="toggle">
      <button id="btnBuy" class="active" type="button">Buy</button>
      <button id="btnSell" type="button">Sell</button>
    </div>
  </div>
  <div class="row">
    <label for="priceIn">Price</label>
    <input id="priceIn" type="number" min="1" max="200" step="1" value="100">
  </div>
  <div class="row">
    <label for="qtyIn">Qty</label>
    <input id="qtyIn" type="number" min="1" max="100" step="1" value="10">
  </div>
  <div class="btns">
    <button id="btnLimit" type="button">Submit limit order</button>
    <button id="btnMarket" type="button" class="ghost">Submit market order</button>
    <button id="btnReset" type="button" class="ghost">Reset</button>
  </div>
</div>
<div class="book-wrap">
  <div class="book-col" id="asks">
    <div class="book-hdr">Asks (Sell)</div>
    <div id="askRows"></div>
  </div>
  <div class="spread-box" id="spreadBox">Spread: —</div>
  <div class="book-col" id="bids">
    <div class="book-hdr">Bids (Buy)</div>
    <div id="bidRows"></div>
  </div>
</div>
<div class="status" id="status">Submit orders to see the matching engine in action.</div>
/* Styling for the order book columns and controls. */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: .9rem; }
.controls { display: flex; flex-direction: column; gap: .45rem; margin-bottom: .7rem; }
.row { display: flex; align-items: center; gap: .5rem; }
label { width: 60px; font-weight: 600; color: #444; font-size: .82rem; }
input[type=number] { width: 90px; padding: .3rem .5rem; border: 1px solid #cdd9e3; border-radius: 6px; font-size: .9rem; }
.toggle { display: flex; gap: 0; border-radius: 6px; overflow: hidden; border: 1px solid #1d3557; }
.toggle button { border: none; border-radius: 0; padding: .3rem .8rem; background: #fff; color: #1d3557; cursor: pointer; font: 600 .83rem system-ui, sans-serif; }
.toggle button.active { background: #1d3557; color: #fff; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 .83rem system-ui, sans-serif; padding: .38rem .8rem; border: 1px solid #1d3557;
         background: #1d3557; color: #fff; border-radius: 6px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
/* Book columns: asks (red) on left, bids (green) on right, spread label in the middle. */
.book-wrap { display: flex; align-items: flex-start; gap: .4rem; margin-top: .4rem; }
.book-col { flex: 1; }
.book-hdr { font-weight: 700; font-size: .78rem; text-align: center; padding: .2rem 0;
            background: #e8eef3; border-radius: 5px 5px 0 0; }
#asks .book-hdr { color: #c92f3c; }
#bids .book-hdr { color: #0a7d33; }
.book-row { display: flex; justify-content: space-between; padding: .18rem .5rem;
            font-size: .82rem; border-bottom: 1px solid #eee; font-variant-numeric: tabular-nums; }
.ask-row { background: #fff5f5; color: #c92f3c; }
.bid-row { background: #f0fff4; color: #0a7d33; }
.spread-box { font-size: .8rem; font-weight: 600; color: #555; writing-mode: horizontal-tb;
              align-self: center; text-align: center; min-width: 54px; padding: .2rem .3rem;
              background: #f5f5f5; border-radius: 5px; border: 1px solid #ddd; white-space: nowrap; }
.status { margin-top: .5rem; font-size: .9rem; font-weight: 600; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.status.info { color: #1d3557; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Matching engine: price-time priority. Incoming limit order crosses the opposite side
// if its price overlaps; remainder rests in the book. Market orders sweep until filled.

// Track which side (buy/sell) the user has selected.
let currentSide = 'buy';
document.getElementById('btnBuy').addEventListener('click', () => setSide('buy'));
document.getElementById('btnSell').addEventListener('click', () => setSide('sell'));
function setSide(s) {
  currentSide = s;
  document.getElementById('btnBuy').classList.toggle('active', s === 'buy');
  document.getElementById('btnSell').classList.toggle('active', s === 'sell');
}

// Two Maps keyed by integer price, each value is an array of resting orders (FIFO queue).
// bids: Map<price, { price, qty, ts }[]>  sorted descending
// asks: Map<price, { price, qty, ts }[]>  sorted ascending
let bids = new Map();
let asks = new Map();
let orderSeq = 0;

function sortedPrices(map, desc) {
  const arr = [...map.keys()].map(Number).sort((a, b) => desc ? b - a : a - b);
  return arr;
}

// matchLimit: attempt to fill qty at price on the given side.
// Returns a status string describing what happened.
function matchLimit(side, price, qty) {
  let remaining = qty;
  const fills = [];
  if (side === 'buy') {
    // Buy limit: sweep asks from lowest price up to our limit price.
    const askPrices = sortedPrices(asks, false);
    for (const ap of askPrices) {
      if (ap > price || remaining <= 0) break;
      const queue = asks.get(ap);
      while (queue.length && remaining > 0) {
        const top = queue[0];
        const filled = Math.min(top.qty, remaining);
        fills.push({ price: ap, qty: filled });
        top.qty -= filled;
        remaining -= filled;
        if (top.qty === 0) queue.shift();
      }
      if (queue.length === 0) asks.delete(ap);
    }
  } else {
    // Sell limit: sweep bids from highest price down to our limit price.
    const bidPrices = sortedPrices(bids, true);
    for (const bp of bidPrices) {
      if (bp < price || remaining <= 0) break;
      const queue = bids.get(bp);
      while (queue.length && remaining > 0) {
        const top = queue[0];
        const filled = Math.min(top.qty, remaining);
        fills.push({ price: bp, qty: filled });
        top.qty -= filled;
        remaining -= filled;
        if (top.qty === 0) queue.shift();
      }
      if (queue.length === 0) bids.delete(bp);
    }
  }
  // Whatever quantity did not fill rests as a new order at our limit price.
  if (remaining > 0) {
    const book = side === 'buy' ? bids : asks;
    if (!book.has(price)) book.set(price, []);
    book.get(price).push({ price, qty: remaining, ts: orderSeq++ });
  }
  return fills;
}

function matchMarket(side, qty) {
  let remaining = qty;
  const fills = [];
  if (side === 'buy') {
    const askPrices = sortedPrices(asks, false);
    for (const ap of askPrices) {
      if (remaining <= 0) break;
      const queue = asks.get(ap);
      while (queue.length && remaining > 0) {
        const top = queue[0];
        const filled = Math.min(top.qty, remaining);
        fills.push({ price: ap, qty: filled });
        top.qty -= filled; remaining -= filled;
        if (top.qty === 0) queue.shift();
      }
      if (queue.length === 0) asks.delete(ap);
    }
  } else {
    const bidPrices = sortedPrices(bids, true);
    for (const bp of bidPrices) {
      if (remaining <= 0) break;
      const queue = bids.get(bp);
      while (queue.length && remaining > 0) {
        const top = queue[0];
        const filled = Math.min(top.qty, remaining);
        fills.push({ price: bp, qty: filled });
        top.qty -= filled; remaining -= filled;
        if (top.qty === 0) queue.shift();
      }
      if (queue.length === 0) bids.delete(bp);
    }
  }
  return { fills, unfilled: remaining };
}

function getSpread() {
  const bestBid = sortedPrices(bids, true)[0];
  const bestAsk = sortedPrices(asks, false)[0];
  if (bestBid == null || bestAsk == null) return null;
  return bestAsk - bestBid;
}

function renderBook() {
  // Re-render the book UI: asks ascending, bids descending.
  const askRows = document.getElementById('askRows');
  const bidRows = document.getElementById('bidRows');
  askRows.innerHTML = '';
  bidRows.innerHTML = '';
  // Render ask levels (lowest price at top = best ask).
  const askPrices = sortedPrices(asks, false);
  for (const ap of askPrices) {
    const totalQty = asks.get(ap).reduce((s, o) => s + o.qty, 0);
    const row = document.createElement('div');
    row.className = 'book-row ask-row';
    row.innerHTML = `<span>${ap}</span><span>${totalQty}</span>`;
    askRows.appendChild(row);
  }
  // Render bid levels (highest price at top = best bid).
  const bidPrices = sortedPrices(bids, true);
  for (const bp of bidPrices) {
    const totalQty = bids.get(bp).reduce((s, o) => s + o.qty, 0);
    const row = document.createElement('div');
    row.className = 'book-row bid-row';
    row.innerHTML = `<span>${bp}</span><span>${totalQty}</span>`;
    bidRows.appendChild(row);
  }
  const sp = getSpread();
  document.getElementById('spreadBox').textContent =
    'Spread: ' + (sp == null ? '—' : sp.toFixed(0));
}

function setStatus(t, cls) {
  const el = document.getElementById('status');
  el.textContent = t;
  el.className = 'status ' + (cls || '');
}

document.getElementById('btnLimit').addEventListener('click', () => {
  const price = parseInt(document.getElementById('priceIn').value, 10);
  const qty = parseInt(document.getElementById('qtyIn').value, 10);
  if (!price || !qty) { setStatus('Please enter a valid price and quantity.', 'bad'); return; }
  const fills = matchLimit(currentSide, price, qty);
  renderBook();
  if (fills.length === 0) {
    setStatus(i18nFmt('Order resting: %side% %qty% @ %price%', { price, qty, side: currentSide === 'buy' ? 'buy' : 'sell' }), 'info');
  } else {
    const totalFilled = fills.reduce((s, f) => s + f.qty, 0);
    const avgPrice = fills.reduce((s, f) => s + f.price * f.qty, 0) / totalFilled;
    setStatus(i18nFmt('Filled %qty% @ avg %avg%', { qty: totalFilled, avg: avgPrice.toFixed(1) }), 'ok');
  }
});

document.getElementById('btnMarket').addEventListener('click', () => {
  const qty = parseInt(document.getElementById('qtyIn').value, 10);
  if (!qty) { setStatus('Please enter a valid price and quantity.', 'bad'); return; }
  const { fills, unfilled } = matchMarket(currentSide, qty);
  renderBook();
  if (fills.length === 0) {
    setStatus('No liquidity on the opposite side to fill a market order.', 'bad');
  } else {
    const totalFilled = fills.reduce((s, f) => s + f.qty, 0);
    const avgPrice = fills.reduce((s, f) => s + f.price * f.qty, 0) / totalFilled;
    if (unfilled > 0) {
      setStatus(i18nFmt('Partially filled %filled% @ avg %avg% — %unfilled% qty unfilled.', { filled: totalFilled, avg: avgPrice.toFixed(1), unfilled }), 'info');
    } else {
      setStatus(i18nFmt('Market order filled: %qty% @ avg %avg%', { qty: totalFilled, avg: avgPrice.toFixed(1) }), 'ok');
    }
  }
});

document.getElementById('btnReset').addEventListener('click', () => {
  bids = new Map(); asks = new Map(); orderSeq = 0;
  renderBook();
  setStatus('Submit orders to see the matching engine in action.', '');
});

// Pre-populate the book with a few resting orders so the demo starts with a visible spread.
function seed() {
  [[102,15],[103,10],[105,25]].forEach(([p,q]) => matchLimit('sell',p,q));
  [[99,20],[98,10],[96,30]].forEach(([p,q]) => matchLimit('buy',p,q));
}
seed();
renderBook();
setStatus('Submit orders to see the matching engine in action.', '');
Result
Console

Notice how the spread — the gap between the best bid and best ask — tightens or widens as you add and remove orders. Narrow spreads mean abundant liquidity; wide spreads signal a thin market where even a small order moves the price.

The Real Complexity

The matching engine sounds simple — sort by price, break ties by time — but its performance demands are extreme. Modern exchanges process millions of order events per second with latencies measured in nanoseconds.

  • Naive sorting is disqualified. Re-sorting the book on every event costs O(nlog⁡n)O(n \log n) per event and collapses under load.
  • The classical solution uses price-indexed queues. Each distinct price level holds a FIFO queue of orders. A hash map or array indexed by price locates any level in O(1)O(1); matching then pops the front of the best queue, also in O(1)O(1) amortized.
  • The full event loop — insert, cancel, match — runs in O(1)O(1) amortized per event on average, O(k)O(k) where kk is the number of levels a market order crosses.
  • Why not trees? A balanced BST gives O(log⁡n)O(\log n) per level lookup but the constant factor is too large for co-located matching engines fighting for nanoseconds. Arrays of queues, lock-free ring buffers, and NUMA-aware memory layouts dominate in practice.
  • The hard problem is not matching but fairness. Exchanges publish detailed matching rules (pro-rata, time-priority, size-priority) and spend years litigating edge cases. The algorithm design challenge is deciding which order fills when several qualify equally.

The limit order book is a case where the algorithmic complexity is solved and the engineering complexity is the frontier. Getting to O(1)O(1) matching was the theory; shaving the last 50 nanoseconds off the hot path is the practice.

Where It Matters

The matching engine pattern appears wherever scarce capacity must be allocated fairly and fast:

  • Equities and futures: every major exchange — NYSE, NASDAQ, CME — runs a variant of the limit order book. The 2010 Flash Crash and the 2022 meme-stock squeezes were both limit-order-book dynamics playing out at scale.
  • Cryptocurrency exchanges: decentralized exchanges (DEXs) replicate the order book on-chain, where "price-time priority" becomes a smart-contract invariant — and gas fees make every match visible on a public ledger.
  • High-frequency trading: HFT firms co-locate servers inside exchange data centers and submit and cancel orders in microseconds, exploiting the O(1)O(1) matching guarantee. The arms race has driven exchange latency below 100 nanoseconds.
  • Network packet scheduling: routers prioritize packets using weighted fair queuing — mathematically identical to a multi-class order book with price replaced by priority class.
  • Task and job queues: operating-system schedulers, cloud job runners, and optimal-testing harnesses all apply some form of priority queue with time-ordering as a tiebreak.

The limit order book is a microcosm of a broader algorithmic idea: priority queues with fairness constraints. Once you understand how a matching engine works, you recognize its skeleton in schedulers, routers, and any system that must serve competing demands in order.

Conclusion

The limit order book is deceptively simple: two sorted lists, one rule — best price wins, ties broken by arrival time. Yet from that rule emerges price discovery, the mechanism by which millions of competing opinions about value collapse into a single number.

The matching engine solved its algorithmic problem decades ago. What keeps researchers and engineers busy is everything around it: fairness in rule design, latency in hardware, stability under stress, and the arms race between market makers and takers.

Next time you see a price on a screen, remember that behind it sits a queue, a priority rule, and a matching loop running faster than a heartbeat — the same idea that governs scheduling in operating systems and max-flow in networks, dressed in the language of markets.

Share this article

Pick a channel — or use your device's native share sheet.

Comments

Loading comments...

https://www.kipuhub.com/en/article/limit-order-book/Content licensed under CC BY-NC 4.0.