pitboss is a tiny deterministic matching engine in C for exploring ordered, replayable systems.
pitboss uses a segmented append-only command journal as its durable input.
The journal is the source of truth: commands are input facts, events are engine decisions, and the book is derived state - rebuilt by replaying the journal, or by loading a checkpoint + retained journal tail.
Scope is narrow: one symbol, limit orders only, fixed capacities, and one mutation owner for sequencing, persistence, matching, and book updates.
See theory.md for limit-order-book background, docs.md for protocol and architecture details, and roadmap.md for current status and planned work.
Some mechanical C/docs/sanity tests and benchmarks were written with non-human assistance. The design/layout/trade offs are mostly human, for better or worse. I've taken some ideas from monoblok.
pitboss is not an exchange. The limit-order book is a compact workload for looking at how ordered systems sequence commands, persist decisions, emit events, replicate logs, and rebuild state.
Matching, persistence, replay, replication, and recovery stay small enough to reason about as one ordered system.
Some of the shape comes from a long-running interest in the ideas around event sourcing and the LMAX architecture.
This project is not trying to build a bad cover version of a 2010 approach in 2026, or an amalgam of every piece of software and every idea I find interesting. Clever people have given this shape a lot of thought; pitboss is meant to be simple and composable.
I want pitboss to approach high performance even though it is only a lab project. C keeps what's happening front and centre.
In C, many of the Java workarounds are plainly nonsense: there is no reason to copy ceremony from a different runtime.
Safety is the obvious push back. Rust is great, and there would be good reason to use it here. For this project, however, I wanted to show the nuts and bolts of a system like this without layers of abstraction or boilerplate hiding the moving parts. It is simple C.
Clients send newline-delimited commands such as
NEW_LIMIT order_id side price qty.
Over TCP, an accepted and stored command returns +OK sequence event-count,
followed by that many event lines. The sequence is the ordered command number; it is
separate from both the order id and the journal byte position used by
replication.
Assume the book already has resting ask order 19: sell 9 at price 101,
and the next journal sequence is 19.
> NEW_LIMIT 41 S 200 10
< +OK 19 2
< ACCEPTED 19 41
< RESTING 19 41 S 200 10
> NEW_LIMIT 42 B 500 10
< +OK 20 3
< ACCEPTED 20 42
< TRADE 20 42 19 101 9
< TRADE 20 42 41 200 1
Order 41 is accepted at sequence 19 and rests because it does not cross.
Order 42 is a buy with limit 500, so it can trade with asks priced 500 or
less. It consumes the cheaper resting ask first: order 19 at maker price
101 for quantity 9, then order 41 at maker price 200 for quantity 1.
The buy is fully filled, so there is no RESTING event for order 42.
cmake -S . -B build
cmake --build buildbuild/pitboss run input.txtUse - for stdin:
printf 'NEW_LIMIT 1 B 100 10\nNEW_LIMIT 2 S 99 4\nCANCEL 1\n' | build/pitboss run -The CLI appends binary command records to a segment journal directory,
pitboss.journal.d by default, before applying them. Set
PITBOSS_JOURNAL=none to disable journaling, or set it to a different
directory. The configured path is a directory name; pitboss will create it when
it does not exist. A path ending in .journal is still accepted if it names a
directory, but examples use .journal.d to avoid implying a single file.
Journal writes use fixed 64-byte records, fixed-size segment files, and a 64
KiB write buffer. Set PITBOSS_JOURNAL_SEGMENT_SIZE=N to choose the segment
data size, and PITBOSS_JOURNAL_RETENTION_SEGMENTS=N to retain only the newest
N segments. The TCP gateway acks accepted commands only after their journal
buffer flushes; the default group-commit timer is 1 ms. Set
PITBOSS_JOURNAL_FLUSH_MS=N to change it, or PITBOSS_JOURNAL_FLUSH_MS=0 to
flush on the next loop turn after accepted work reaches the sequencer. Set
PITBOSS_JOURNAL_FSYNC=1 to fsync each flushed buffer. Gateway flush and fsync
work runs off the libuv loop, but durable ack throughput is still bounded by the
storage device's sync latency and group size.
With unlimited retention, the command journal is the canonical input. With bounded retention, recovery requires a checkpoint baseline plus the retained segment tail; do not enable bounded retention unless snapshots/checkpoints are kept as the baseline for pruned history.
Replay a sequenced journal and print the same events again:
build/pitboss replay pitboss.journal.dThe replay output format is described in docs.md.
Dump the derived book state after replaying a journal:
build/pitboss dump pitboss.journal.dbuild/pitboss dump without a path reads PITBOSS_JOURNAL when it is set,
otherwise pitboss.journal.d.
Create a binary checkpoint of the current derived book, then recover from that checkpoint plus any later journal records:
build/pitboss checkpoint pitboss.journal.d pitboss.snap
build/pitboss recover pitboss.journal.d pitboss.snapWhen run or listen append to an existing segment journal, pitboss first
replays retained segments silently so sequence numbers and the in-memory book
continue from the recovered state. If old segments have been pruned, startup
needs a checkpoint baseline before the retained tail.
The TCP listener is an I/O shell around the deterministic core. Socket framing, parse work, and stateless validation can run outside the sequencer; sequence assignment, journal append, matching, book mutation, monitor fanout, and replication fanout stay ordered on the libuv loop.
flowchart LR
client["client commands"]
gate["gate / validation"]
sequencer["sequencer"]
journal_append["journal append"]
journal_flush["journal flush"]
matcher["matcher"]
events["events"]
book["book state"]
monitors["monitor clients"]
replicas["warm followers"]
client --> gate --> sequencer --> journal_append --> journal_flush --> matcher --> events --> book
events --> monitors
journal_flush --> replicas
build/pitboss listen 127.0.0.1 17077
build/pitboss listen 127.0.0.1 17077 17078
build/pitboss listen 127.0.0.1 17077 17078 17079
scripts/pitboss-client.py 127.0.0.1:17077
scripts/pitboss-client.py 127.0.0.1:17077 -c 'NEW_LIMIT 1 B 100 10'
scripts/start-replication-pair.shThe TCP listener handles SIGINT and SIGTERM as graceful shutdown requests:
it closes the listeners and active sessions, then runs the normal journal close
path. Set PITBOSS_JOURNAL_FSYNC=1 when flushed journal buffers should be
fsynced.
When the optional monitor port is present, monitor clients receive
PITBOSS MONITOR 1, a current book snapshot, then a read-only live event
stream.
When the optional replication port is present, warm followers can connect and tail the primary's sequenced journal bytes to maintain their own segment journal.
PITBOSS_JOURNAL=follower.journal.d build/pitboss follow 127.0.0.1 17079The follower recovers its local journal, sends a byte/sequence cursor, then appends and applies primary record bytes after that point. If the cursor is older than the primary's retained segment floor, the follower needs a checkpoint baseline before it can tail the retained journal. Client ingress remains single-primary.
For operator-driven promotion of follower to leader, inspect the follower journal and compare it with
the old primary monitor sequence. See scripts/start-replication-pair.sh and the temp utility scripts it generates.
ctest --test-dir build
scripts/listener-smoke.sh
scripts/monitor-smoke.sh
scripts/replication-smoke.shProtocol, matching semantics, architecture notes, test commands, and current limitations live in docs.md.
