An order book that survives kill -9.

C# order matching with a write-ahead log, group commit and live trades.

tickflow's live screen: an order ticket, the ACME order book with depth bars, a price chart and the trade tape
The live screen, fed over a WebSocket. Plain JavaScript, no build step.

Crash test result

0 lost of 43,808 acknowledged orders

Test
python/crash_test.py
Method
10 rounds of kill -9 while 32 clients place orders, then restart and re-send every acknowledged order
Mode
Durability = Group

Same guarantee, 55× the throughput

One fsync covers every order in a batch, and each client is acknowledged only after it.

Latency; end-to-end rows at 64 clientsApple M2, .NET 8, Release
PathOrders/sp50p99p99.9Survives
Matching engine alone476,3670.3 µs2.2 µs6.0 µsn/a
End to end, no fsync148,8000.32 ms2.9 ms20.8 msprocess crash
End to end, group commit20,3183.0 ms6.0 ms30.3 mspower loss
End to end, fsync per order372189 ms210 ms210 mspower loss

End-to-end latency is mostly queueing: with 64 orders in flight, p50 is about 64 ÷ throughput (Little’s law). The engine’s worst call in a million, 153 ms, was a GC pause. “Power loss” follows from the fsync design; the crash test kills the process, not the power.

One thread owns the book

No locks in matching, and the log order is exactly the execution order.

  1. Queue

    Requests arrive over a bounded channel; callers wait when it is full.

  2. Append to the log

    Each record is [length][CRC32][payload].

  3. Match

    Price-time priority, integer ticks, O(1) cancel lookup.

  4. Fsync once per batch

    Up to 256 queued orders share one fsync of about 3 ms. Under light load a batch is one order.

  5. Acknowledge the batch

    Trades go to the WebSocket feed and SQLite.

Replay

On start the log is replayed; a torn last record fails its checksum and is cut.

Idempotent retries

The same clientOrderId returns the original result, even after a restart.

SQL bars

Minute OHLC and VWAP with window functions over 1M trades. Last hour: 16.3 ms with the index, 82.2 ms without. A full day: 1.1×.

28 tests

Matching, recovery in every durability mode, torn logs, HTTP across a restart.

Reproduce it

The numbers above come from these commands. One laptop; the crash test starts its own server.

Terminal.NET 8, Python 3
# 28 tests
dotnet test
# latency and SQL tables
dotnet run -c Release --project bench/Tickflow.Bench -- results
# kill -9 test, 10 rounds
cd python && uv venv && uv pip install aiohttp pandas matplotlib
.venv/bin/python crash_test.py --rounds 10