Introduction
matching-engine processes spot orders with price-time priority. The Rust workspace separates core types, matching, shard orchestration, persistence and network delivery. TCP and Unix domain socket clients use binary protocol v2.
Supported orders include limit, market, stop-limit, stop-market, trailing-stop and iceberg. Time-in-force choices are GTC, IOC, FOK and GTD. Risk policy is configured per symbol; session ownership scopes cancel, amend and status queries.
Start with the quick start. Integrators should read the protocol and recovery contract before implementing reconnects.
The service provides matching, not account balances or settlement. There is no replication, consensus or automatic failover. Journals and durable request history grow with activity. Network latency depends on storage sync, TLS, workload and hardware; benchmark the complete path before setting an SLA.
Quick start
Install Rust 1.96 or newer. Clone the repository and build the TLS-capable server:
git clone https://github.com/rekurt/matching-engine.git
cd matching-engine
cargo build --release --locked -p me-server --features tls
./target/release/me-server --config config.toml
The sample config defines BTC/USDT, ETH/USDT and SOL/USDT. Trading listens on 127.0.0.1:9090, HTTP on 127.0.0.1:9091, and the WAL directory is ./data/wal. TLS is available in the binary but is enabled only when certificate paths are configured.
In a second terminal, check readiness and run the Go example:
curl --fail http://127.0.0.1:9091/ready
cd examples/go
go run . 127.0.0.1:9090
The example places crossing limit orders, reads their execution events and cancels a resting order. Run it against a fresh test data directory: previously used order IDs remain part of durable history.
Stop the server with SIGINT or SIGTERM. On restart, use the same symbol/risk configuration, shard topology and whole data directory. See deployment before changing recovery settings or exposing listeners remotely.
For a temporary in-memory experiment, set [wal] enabled = false in a separate config. That disables crash recovery and is not a durable deployment.
Configuration
Pass --config config.toml. Without it, the binary uses built-in defaults. CLI overrides are --tcp-addr, --metrics-addr, --shards and --uds-path. Unknown TOML fields and invalid combinations are rejected before trading starts.
Server
| Field | Default | Meaning |
|---|---|---|
server.tcp_addr | 127.0.0.1:9090 | Trading listener |
server.metrics_addr | 127.0.0.1:9091 | HTTP health, metrics and admin |
server.shards | 0 | Auto CPU count; pin a count for recovery compatibility |
server.uds_path | absent | Optional Unix domain socket |
server.allow_plaintext_remote | false | Explicit opt-in for non-loopback plaintext TCP |
server.clock | coarse | system, coarse or rdtsc |
server.pin_threads | false | Linux CPU affinity |
server.shutdown.drain_timeout_secs | 30 | Connection drain deadline |
server.heartbeat.interval_ms | 5000 | Advertised heartbeat interval |
server.heartbeat.timeout_ms | 15000 | Connection read timeout |
server.session.replay_buffer_size | 1000 | Per-session replay frame capacity |
server.session.ttl_secs | 60 | Idle lifetime for unused sessions |
Configure server.tls.cert_path and key_path together; client_ca_path enables required client certificates. TLS needs the tls build feature. The HTTP listener has no native TLS.
server.admin.api_key is empty by default, disabling halt/resume routes. Configured keys require at least 16 printable, non-space ASCII characters. Status and metrics remain public; protect the HTTP listener with a private network or proxy.
Symbols and risk
Each [[symbols]] entry defines an ID, base/quote names, price/quantity scales and decimal-string limits. See trading pairs. The optional [symbols.risk] table enables these controls:
| Field | Meaning |
|---|---|
max_order_qty | Maximum order quantity, including hidden quantity |
max_order_notional | Conservative quote notional cap |
price_band_bps | Deviation from reference price; zero disables |
reference_price | Initial reference price as a decimal string |
max_orders_per_second | Token-bucket limit; zero disables |
max_open_orders | Resting-order limit; zero disables |
trading_enabled | Admission switch |
self_trade_prevention | Reject same-session self-matches |
Without a risk table, these checks do not run. Market orders subject to notional/reference checks fail closed if no reference price is available. Seed reference_price or establish a traded reference. Position limits are not supported without an external account ledger.
invariant_policy is halt_symbol by default. strict panics on internal invariant violations; continue logs and continues and should be reserved for controlled recovery.
Storage
| Field | Default | Meaning |
|---|---|---|
wal.enabled | true | Enable durable recovery |
wal.mode | pre | pre or post; both persist decisions/outcomes before reply |
wal.path | ./data/wal | Journal directory |
wal.sync_interval | 100 | Legacy writer interval; the server syncs each decision explicitly, so this does not batch server replies |
wal.snapshot_dir | derived | Defaults to {wal.path}/snapshots |
wal.snapshot_interval | 100000 | Commands per shard checkpoint; zero disables |
Snapshot creation does not truncate decision history. Symbol/risk settings and shard topology are part of the recovery contract. See deployment and recovery for backups and upgrades.
Trading pairs
Each [[symbols]] entry registers one symbol. IDs must be unique. Prices and quantities on the wire always use scale 10^8; price_scale and qty_scale do not change the wire scale.
[[symbols]]
id = 0
base = "BTC"
quote = "USDT"
price_scale = 2
qty_scale = 8
min_qty = "0.00001"
max_qty = "10000.0"
qty_step = "0.00001"
price_step = "0.01"
invariant_policy = "halt_symbol"
[symbols.risk]
max_order_qty = "10000.0"
max_order_notional = "100000000.0"
price_band_bps = 1000
reference_price = "50000.0"
max_orders_per_second = 1000
max_open_orders = 100000
trading_enabled = true
self_trade_prevention = false
Limits and steps are decimal strings, parsed without binary floating-point rounding. Quantity must be positive, within min/max bounds and a multiple of qty_step. Limit prices must be positive and a multiple of price_step.
For example, 50000.50 is raw price 5000050000000 and 0.5 is raw quantity 50000000, regardless of symbol display scales. Use checked integer/decimal conversion in clients.
Adding or changing symbols, risk settings or shard count in an existing durable deployment can make recovery incompatible. Validate against copied storage and follow the upgrade procedure.
Client integration
Connect through TCP or UDS, then send Handshake or ResumeSession before order commands. The wire specification defines framing, payloads and sequence numbers.
Keep one ordered reader per connection. Correlate solicited events by request_id; passive fills and stop events use request ID zero. A command can produce several events. An order snapshot ends at OpenOrdersEnd, not at a period of socket silence. Handle fragmented reads and multiple frames in one read. Preserve passive events that arrive before or during a snapshot; commit the snapshot cursor only after validating Begin, End and the declared count.
Persist the session ID and the last applied sequence from the replay stream, as defined in the protocol. Positive-sequence control frames also belong to replay; process and acknowledge them in order. Sequence-zero frames, including SessionResumed, never advance the cursor. Resume with the saved cursor, ignore duplicate replayed events, acknowledge only applied replay frames and retain the original request ID/payload when retrying a mutation. Session IDs are bearer credentials; use TLS and account authorization at the gateway.
If ReplayGap occurs, request GetOpenOrders and validate the complete Begin/Order/End response. That restores open orders, not missed settlement history; reconcile executions against an authoritative ledger before resuming financial processing. Batches are not atomic across symbols.
Supplied clients
| Client | Use |
|---|---|
| Go SDK | TCP/UDS handshake, submission and event APIs |
| Python example | Standard-library protocol demonstration |
| TypeScript example | Node.js Buffer framing and event parsing |
| Rust example | Tokio framing and basic order flow |
Examples live under examples/ in the repository. They are demonstrations, not complete reconnect, account or settlement services. The Go SDK does not expose a full high-level session resume/receive cursor/original request-ID recovery API.
See deployment for ownership, durable retries, admission limits and storage requirements.
Go client
The SDK is in clients/go/meclient; the runnable example is in examples/go.
cd examples/go
go run . 127.0.0.1:9090
Dial and DialUDS perform Handshake automatically. For TLS, establish a tls.Conn, pass it to NewClientFromConn and call Handshake before commands.
Use decimal strings for exact prices and quantities (NewDecimalFromString). Avoid floating-point conversion for financial input. ReadEvent returns individual events; ReadEvents provides streaming delivery.
The SDK serializes socket reads and writes, but Ping and Handshake consume their own replies. Do not run them concurrently with the event reader. Use one reading mode per connection.
The SDK supports basic submission/event operations for protocol v2. It does not provide a complete high-level ResumeSession/cursor/original-request-ID recovery API. A gateway must implement the recovery contract before relying on reconnects.
Python example
examples/python/client.py demonstrates TCP framing with Python's standard library (socket, struct and decimal conversion). It requires Python 3.10 or newer.
python3 examples/python/client.py 127.0.0.1 9090
The example performs Handshake, submits orders on symbol 1 and reads events. It is intended for a fresh local test server. Its optional arguments are host and port; adapt its symbol and order values before using it with a different configuration.
Run its protocol tests with python3 -m unittest discover -s examples/python. The separate scripts/runtime_smoke.py test client exercises actual TCP/mTLS processes, crash recovery and session resume.
For production integration, implement persistent receive cursors, mutation retries and settlement reconciliation as described in client integration.
Wire protocol v2
TCP and Unix domain sockets carry the same frames. TLS changes the transport, not the frame layout. All integers are little-endian. Prices use signed i64 and quantities use unsigned u64, both scaled by 10^8. Timestamps and expiry times use Unix nanoseconds.
Frame header
| Offset | Size | Field |
|---|---|---|
| 0 | 4 | Payload length, excluding the header (u32) |
| 4 | 1 | Version: 2 |
| 5 | 1 | Message type |
| 6 | 8 | Sequence (u64) |
| 14 | 8 | Request ID (u64) |
| 22 | variable | Payload |
Read exactly the 22-byte header, then the declared payload. One socket read may contain part of a frame or several frames. Regular inbound payloads are limited to 1024 bytes; batch payloads to 4096 bytes.
Client sequences increase strictly within a connection. Sequence zero is accepted only for its first Handshake. Commands and queries require a nonzero request ID; Ping, Handshake, ResumeSession and Acknowledge do not. Reuse the original request ID and exact payload when retrying a mutation.
Positive server sequences identify the per-session replay stream. Replies passed through the active-session response path, including HandshakeAck, Pong, ReplayGap from ResumeSession and some Error frames, are assigned server sequences and journaled when WAL is enabled. A gap detected during pending delivery instead emits ReplayGap directly with sequence zero, outside the replay journal. Replayed frames retain their original sequence. The server rewrites these headers; a Pong header is not the echoed client sequence (its payload reports the last processed client sequence).
Sequence zero is out-of-band and never advances the receive cursor. SessionResumed is sent with zero before the replay frames; errors sent outside the active-session replay path also use zero. Process every positive-sequence frame in order, including control frames, and advance the cursor only after applying it. Solicited order events mirror the request ID; passive owner events use zero.
Connection and recovery
Start with Handshake to create a session, or ResumeSession to recover one. Do not send order commands before acceptance. Persist the session ID and last applied positive server sequence outside the connection. A session can have only one active connection.
| Type | Name | Payload, in field order |
|---|---|---|
0x04 | Ping | Empty, or last applied server sequence (u64) as cumulative ACK |
0x05 | Handshake | Empty, or client version (u8, use 2) |
0x06 | ResumeSession | Session ID (16 bytes), last received/applied server sequence (u64) |
0x08 | Acknowledge | Last applied server sequence (u64) |
0x18 | Pong | Last processed client sequence (u64): 8 bytes |
0x19 | Error | Code (u16), message length (u16), UTF-8 message |
0x1A | HandshakeAck | Server version (u8), TLV extensions |
0x1B | SessionResumed | Replayed frame count (u32) |
0x20 | ReplayGap | Missing sequence range: from (u64), to (u64) |
0x23 | FlowControlWarning | Buffer usage percent (u8), hard-limit flag (u8) |
HandshakeAck currently has 41 payload bytes. Each TLV is tag (u16), length (u16), value. Skip unknown tags:
| Tag | Value |
|---|---|
1 | Capabilities (u64) |
2 | Assigned session ID (16 bytes) |
3 | Heartbeat interval in milliseconds (u32) |
Capability bits 0–7 indicate session recovery, heartbeat, batch orders, order flags, order query, TLS, mutation deduplication and open-order snapshots respectively. Send heartbeats within the configured timeout.
SessionResumed precedes replay. On ReplayGap, obtain a complete GetOpenOrders snapshot; it restores open order state, not missed settlement events. Reconcile missing executions separately. Do not silently treat a fresh session as recovery of an old one. See client integration and deployment.
Order commands
PlaceOrder (0x01)
| Offset | Type | Field |
|---|---|---|
| 0 | u64 | Nonzero order ID |
| 8 | u16 | Symbol ID |
| 10 | u8 | Side: buy 0, sell 1 |
| 11 | u8 | Order type |
| 12 | u8 | Time in force |
| 13 | i64 | Limit price; zero for market |
| 21 | i64 | Stop price; zero for non-stop |
| 29 | u64 | Positive total quantity |
| 37 | u8 | Optional flags; absent means zero |
| 38 | u64 | Optional GTD expiry; absent means zero |
| 46 | i64 | Optional trailing distance; absent means zero |
| 54 | u64 | Optional iceberg display quantity; absent means zero |
Base payload is 37 bytes; optional fields extend it to 38, 46, 54 or 62 bytes. Include all preceding optional fields when using a later one. GTD requires a nonzero future expiry. Trailing distance must be nonnegative and a positive distance is valid only on stop orders. Positive display quantity is valid only on limit orders. Stop orders require a positive stop price; limit/stop-limit require a positive limit price.
Order types: Limit 0, Market 1, StopLimit 2, StopMarket 3. Time in force: GTC 0, IOC 1, FOK 2, GTD 3. Flags: POST_ONLY 0x01, REDUCE_ONLY 0x02, MAKER_OR_CANCEL 0x04. REDUCE_ONLY is a reserved futures flag; this spot engine does not enforce position reduction. Do not rely on it as an account risk control.
Other commands
| Type | Name | Payload |
|---|---|---|
0x02 | CancelOrder | Order ID (u64), symbol ID (u16): 10 bytes |
0x03 | AmendOrder | Order ID (u64), symbol ID (u16), has-price (u8), optional price (i64), has-qty (u8), optional qty (u64): 12–28 bytes |
0x07 | GetOrderStatus | Order ID (u64), symbol ID (u16): 10 bytes |
0x09 | GetOpenOrders | Empty |
0x0B | BatchPlaceOrder | Count (u8, 1–20), then fixed 38-byte PlaceOrder entries |
0x0C | BatchCancelOrder | Count (u8, 1–50), then 10-byte CancelOrder entries |
Amend presence flags are 0 or 1; include at least one changed field. Batch placement requires the flags byte on every entry and cannot carry GTD expiry, trailing distance or iceberg extensions. Batches execute individual items and are not atomic across symbols.
Cancel, amend and status requests are scoped to the requesting session. An order belonging to another session is reported as not found. Durable mutation retries return the original outcome; a retained request ID with a different payload is rejected. Query responses may be regenerated after bounded-cache eviction.
Order events
Fields below are serialized in the listed order, without alignment padding. timestamp is u64, price is i64, quantity is u64, and order/trade IDs are u64.
| Type | Name | Bytes | Payload |
|---|---|---|---|
0x10 | OrderAccepted | 16 | Order ID, timestamp |
0x11 | OrderRejected | 17 | Order ID, reason (u8), timestamp |
0x12 | Trade | 59 | Trade ID, symbol (u16), price, qty, maker ID, taker ID, maker side (u8), timestamp, trade sequence (u64) |
0x13 | OrderFilled | 32 | Order ID, filled qty, price, timestamp |
0x14 | OrderPartiallyFilled | 40 | Order ID, filled qty, remaining qty, price, timestamp |
0x15 | OrderCancelled | 24 | Order ID, remaining qty, timestamp |
0x16 | OrderAmended | 32 | Order ID, new price, new qty, timestamp |
0x17 | StopTriggered | 24 | Order ID, trigger price, timestamp |
0x1D | OrderStatus | 41 | Order ID, status (u8), filled qty, remaining qty, price, timestamp |
0x1E | OrderNotFound | 8 | Order ID |
One command can emit several events. Continue consuming unsolicited owner events even when no local command is pending. The trade sequence inside Trade is distinct from the frame's server sequence.
Open-order snapshots
| Type | Name | Bytes | Payload |
|---|---|---|---|
0x1F | OpenOrdersBegin | 12 | Count (u32), snapshot sequence (u64) |
0x21 | OpenOrder | 60 | Layout below |
0x22 | OpenOrdersEnd | 8 | Same snapshot sequence (u64) |
Wait for End with the matching request ID. Validate Begin/End sequence equality and the declared order count before replacing local state. A read timeout is not a valid empty snapshot.
OpenOrder offsets: ID 0 (u64), symbol 8 (u16), side 10, type 11, TIF 12, status 13 (u8 each), price 14 (i64), stop price 22 (i64), total qty 30 (u64), open qty 38 (u64), timestamp 46 (u64), flags 54 (u8), reserved zero bytes 55–59. For an iceberg, open qty includes the hidden remainder. This fixed layout does not include expiry, trailing distance or display size.
Order status values: New 0, PartiallyFilled 1, Filled 2, Cancelled 3, Rejected 4, Triggered 5.
Rejection and error codes
OrderRejected reasons (u8):
| Code | Reason | Code | Reason |
|---|---|---|---|
| 0 | NoLiquidity | 8 | SymbolNotFound |
| 1 | InsufficientLiquidity | 9 | TradingHalted |
| 2 | InvalidPrice | 10 | RiskCheckFailed |
| 3 | InvalidQuantity | 11 | WouldTakeMarket |
| 4 | InvalidLotSize | 12 | InternalError |
| 5 | InvalidTickSize | 13 | SelfTradePrevented |
| 6 | OrderNotFound | 14 | InvalidExpiry |
| 7 | DuplicateOrderId | 15 | AmendNotSupported |
Protocol Error codes (u16): 1 UnsupportedVersion, 2 SequenceOutOfOrder, 3 HandshakeRequired, 4 InvalidPayload, 5 ServerShuttingDown, 6 Backpressure, 7 HeartbeatTimeout, 8 TlsError, 9 InvalidRequestId, 10 SessionExpired, 11 BufferExhausted, 12 InternalError. Some codes are reserved; their assignment remains part of the wire contract. Handle unknown codes without corrupting framing.
An InternalError is not a successful execution acknowledgment. Reconnect/retry according to the retained mutation identity and recovery contract; do not generate a replacement order merely because delivery failed.
Architecture
The service separates symbol matching from network I/O. Each symbol maps to a shard worker; that worker serializes commands, risk decisions and book mutations. Independent symbols have no global execution order.
Workspace
| Crate | Responsibility |
|---|---|
me-core | Fixed-point types, orders, events, symbols and session IDs |
me-book | Price levels, order pool, stop book and price-time matching |
me-engine | Per-symbol engine, routing, shard workers, risk/durability orchestration |
me-persist | Journal encoding, CRC checks and atomic snapshots |
me-io | Protocol codec, TCP/UDS, session storage, replay and delivery |
me-server | Configuration, startup/recovery, TLS, HTTP and shutdown |
me-risk | Per-symbol admission and execution risk policy |
me-metrics | Prometheus instrumentation |
me-audit | Structured audit telemetry |
me-clock | System, coarse and CPU-counter clocks |
me-bench | Criterion benchmark suites |
me-datagen | Seeded order-flow generator |
Matching
The order book uses sorted price levels, FIFO order links and indexed order storage. Crossing orders take the best opposing prices first. Cancels use the order index; price/quantity amendments can alter time priority according to the matching rules. Stops activate from trade prices, GTD orders expire from recorded processing time, and iceberg replenishment participates in the same matching work budget.
Prices are signed 64-bit fixed-point values; quantities are unsigned 64-bit fixed-point values, both with scale 10^8. The binary wire layout is explicitly encoded and is independent of Rust struct layout.
Risk policy runs before admission and also covers amendments and triggered stops. Ownership is based on the client session. Internal invariant violations use the configured policy, defaulting to halting the affected symbol.
Request path
- Decode a frame and validate protocol/connection state.
- Check session/request identity and admission capacity.
- Route the command to its symbol worker, enforcing session ownership and risk.
- Persist the decision/outcome when durability is enabled.
- Persist replies and passive owner events in session history before delivery.
Network tasks use blocking-task boundaries for synchronous shard operations. TCP and UDS share codec and session semantics; TLS wraps TCP transport.
Recovery and observability
Engine journals and the session journal are a single recovery unit. Snapshots are checkpoints; retained decisions reconstruct subsequent state and verify expected events. Startup fails on incompatible configuration, detected corruption or replay divergence.
HTTP readiness reflects listener, session-storage and engine health. Intentional symbol halts are reported by status without making the service unhealthy. Audit telemetry may drop records under pressure and is not the authoritative execution journal. See deployment for the operational contract and HTTP API for endpoints.
HTTP API
The HTTP listener uses server.metrics_addr, default 127.0.0.1:9091. It provides no native TLS. Protect it with a private network or authenticated proxy.
| Method | Path | Authentication | Result |
|---|---|---|---|
| GET | /health | none | 200 OK or 503 when unavailable/draining |
| GET | /ready | none | Same availability check as health |
| GET | /metrics | none | Prometheus text exposition |
| GET | /admin/status | none | Service status and per-symbol trading state |
| POST | /admin/halt/{symbol_id} | X-Admin-Key | Durable halt |
| POST | /admin/resume/{symbol_id} | X-Admin-Key | Durable resume |
Halt/resume routes are not registered when server.admin.api_key is empty; requests then return 404. With a configured key, missing/incorrect credentials return 401. Keys require at least 16 printable, non-space ASCII characters. /admin/status remains public even when a key is configured.
curl --fail http://127.0.0.1:9091/ready
curl --fail http://127.0.0.1:9091/admin/status
curl --fail -X POST -H "X-Admin-Key: $ME_ADMIN_KEY" http://127.0.0.1:9091/admin/halt/0
/health and /ready return plain text: OK on success, or draining / engine unavailable with HTTP 503. They do not return JSON.
GET /admin/status example when the service is ready:
{"status":"ready","symbols":[{"symbol_id":0,"trading_enabled":true}]}
Unavailable/draining /admin/status returns HTTP 503 with {"status":"unavailable"} or {"status":"draining"}. Health also checks trading listeners, session storage and shard workers. A deliberately halted symbol remains operationally healthy.
Halt/resume responses contain symbol_id, action and success. Success returns 200; an unknown symbol returns 404, and a failed engine operation returns 503. Existing orders remain in the book when a symbol is halted.
For order commands and events, use the binary protocol.
Deployment and recovery
This guide describes single-node deployment, storage compatibility and client recovery. Engine benchmarks exclude durable network service costs.
Build and exposure
Use Rust 1.96 or newer and cargo build --release --locked -p me-server --features tls. Run with --config config.toml. Configuration rejects unknown fields and invalid combinations. TCP and HTTP default to loopback. Non-loopback plaintext TCP requires the explicit server.allow_plaintext_remote = true setting; prefer TLS:
[server]
tcp_addr = "0.0.0.0:9090"
metrics_addr = "127.0.0.1:8080"
shards = 4
[server.tls]
cert_path = "/etc/me/server.pem"
key_path = "/etc/me/server.key"
client_ca_path = "/etc/me/client-ca.pem"
[server.admin]
api_key = "replace-with-a-long-random-secret"
[wal]
enabled = true
mode = "pre"
path = "/data/wal"
snapshot_interval = 100000
Add the required [[symbols]] definitions from the root configuration. Setting client_ca_path requires authenticated client certificates. TLS configuration on a binary without the TLS feature, incomplete certificate settings, and unreadable/invalid credentials prevent startup before trading listeners are exposed. A session ID is a bearer credential; TLS does not replace account authorization at the gateway. Protect HTTP metrics and admin access separately: this listener does not implement TLS. An empty admin key disables mutations; configured keys must contain at least 16 printable non-space ASCII characters.
The Dockerfile builds TLS support, runs as UID 10001, and checks /ready on port 8080. Mount a persistent writable /data volume and a read-only configuration/certificate directory. Its default loopback TCP binding requires an explicit deployment configuration for remote clients. With its ENTRYPOINT, pass --config /etc/me/config.toml --metrics-addr 127.0.0.1:8080 as container arguments, without another me-server executable. Restrict port publishing to the intended network. The mounted data directory must be writable by UID 10001.
Durability and recovery
Keep WAL enabled for durable operation. Both pre and post modes synchronously persist mutation decisions and their outcomes before replying. Replay uses the stored decision, original ownership, processing time, expiration metadata, and expected events; it does not turn rejected or unauthorized commands into privileged successful commands.
Engine journals and sessions.wal form one recovery unit. Session admission, request fingerprints, batch intents, replies, and acknowledgments are journaled. Never delete or replace only one of these files. Missing session history for an existing engine history prevents startup. Journal files containing bearer credentials are restricted to owner read/write permissions. A writer lock prevents two engines from recovering or mutating the same journal concurrently.
Snapshots are checkpoints. The complete append-only decision history is retained: snapshot creation does not truncate the journal. A corrupt snapshot can be bypassed only when complete validated history can reconstruct state. Detected engine journal corruption, incompatible configuration, or replay divergence prevents startup. The session journal recovers an incomplete trailing frame at the last complete record. CRC and contiguous sequence validation do not prove that entire trailing records were never deleted; consistent whole-directory backups and storage integrity remain required. Symbol/risk configuration and shard topology are recorded; incompatible changes prevent recovery. Pin shards instead of relying on machine-dependent automatic CPU counts.
Upgrade boundary: old command-only/compacted journals are not silently migrated into the decision format. Do not replace the binary over an existing legacy data directory and assume compatibility. Stop intake, reconcile outstanding orders and executions with the upstream ledger, preserve a complete backup, and use an explicitly validated migration or a reconciled empty book. There is no automated legacy migration or online resharding tool. The atomic snapshot response adds a session-journal record variant: after the new binary writes one, an older binary cannot read that journal. Rollback must use a compatible reader or a reconciled backup; never restore an old backup over acknowledged new trades.
For backups, stop intake and shut down cleanly, then copy the whole data directory with its permissions and deployment configuration. Test restoration in an isolated environment before relying on a backup. A live copy of individual files is not a consistent backup.
Client recovery contract
Protocol v2 uses a 22-byte header and client request IDs. Persist the session ID and last applied sequence from the replay stream on the client. Reconnect by resuming that session; concurrent use of the same session is refused. Retrying an identical durable mutation returns its original outcome; reusing a retained request ID with a different payload is rejected. Read-only query responses have a bounded cache and can be regenerated after eviction.
The replay stream includes both order/query responses and positive-sequence control frames, as described in the protocol. Replayed frames retain their original sequence; never apply them twice. Sequence zero is out-of-band and is not acknowledged. SessionResumed precedes replay with sequence zero. HandshakeAck, Pong, ReplayGap from ResumeSession and some Error frames in the active-session response path receive positive server sequences and must be processed before advancing the cursor. ReplayGap emitted during pending delivery instead uses sequence zero and is not journaled. Passive fills, trades, and stop events use request_id = 0 and are delivered to their owners, including when the owner reconnects later. Preserve those events while assembling a snapshot and commit its cursor only after the complete response validates.
On ReplayGap, request GetOpenOrders. Complete snapshots are delivered in bounded write chunks even when larger than the replay ring. A snapshot is limited to 8 MiB of encoded frames, enough for the 100,000 admitted orders of one session; oversized snapshots are refused before journal mutation. A complete snapshot is persisted as one session-journal record before delivery; interruption during its append does not recover a prefix as a completed snapshot. The per-session query-response cache has a 16 MiB byte budget, with query responses and their admission metadata evicted together. This does not evict durable mutation decisions. An open-order snapshot restores current order state, not missing historical settlement events. Reconcile those against an authoritative execution record before resuming financial processing. The example clients demonstrate submission; they are not complete account/settlement recovery services. The Go SDK does not yet expose a high-level ResumeSession/receive-cursor/original-request-ID recovery API; implement the recovery protocol at the gateway before relying on reconnect for financial processing.
Batches retain individual durable item identities and recover partially completed execution without repeating completed items. They are not an atomic transaction across symbols. Per-symbol execution and delivery are serialized; there is no global order across independent symbols.
Capacity and risk policy
Request history and the append-only journals currently grow with activity. Replay buffers are bounded, but total durable request history is not a constant-memory store. Monitor process memory, disk space, write latency, and restart duration; provision capacity and schedule reconciled maintenance. Online history compaction/retention is not implemented. Benchmark the durable path on the intended filesystem and workload before setting an SLA; synchronous persistence changes the cost relative to an in-memory benchmark.
Session admission is bounded: at most 10,000 sessions; each session has 100,000 place/amend units and 16 MiB of admitted mutation payload, with a separate 100,000-unit cancellation reserve. Batches count individual items. Invalid/unauthorized cancellation attempts do not consume that reserve. Existing request retries remain usable. Unused idle sessions can expire; sessions with durable activity are retained to preserve ownership and idempotency. Plan session lifecycle and capacity at the gateway; expiration is not a way to discard active trading history.
Risk checks cover amendments and activated stops as well as placements. A market order requiring a reference price fails closed when none is available; seed symbols.risk.reference_price explicitly or establish a traded reference. Hard notional checks use a conservative execution-price bound from the book and total quantity, including hidden quantity. They can reject an IOC that would only partly fill. With a hard notional cap, amendments after any fill may only decrease or preserve both price and quantity; increases return AmendNotSupported because historical filled quote notional is not an account ledger. Zero rate limit disables that limit. Position limits without an account ledger are unsupported and rejected rather than advertised as enforced.
Matching work limits
A matching command has a shared budget of 4,096 fills, including triggered orders and iceberg replenishment. An order whose predicted matching work exceeds the remaining budget is rejected before that matching pass mutates the book; FOK does not partially execute. An iceberg may contain at most 4,096 display slices. Each symbol may have at most 4,096 live pending stops and 4,096 live GTD orders. These limits apply alongside any stricter configured risk limits and are checked during snapshot restoration. Retired/cancelled orders release their capacity.
The preflight is conservative and can reject work that a less conservative simulation might admit. Existing snapshots above these limits are rejected. Journals created with different matching semantics, including the former delayed iceberg refill behavior, may fail event-equivalence replay; validate upgrade and reconciliation against a copied data directory before deployment.
Matching corrections also preserve FIFO after snapshot restoration, remove the full visible remainder when an amendment cancels a filled-down order, and consume each trade price only once when advancing trailing stops. Journals containing decisions produced by the former behavior can fail equivalence replay. Validate the new binary against a copy of existing storage before upgrading; do not discard or rewrite acknowledged executions to bypass a mismatch.
These bounds prevent a tiny display quantity or a large stop/expiry wave from expanding one command into an oversized durable decision. They are service capacity limits, not financial account limits. Gateway admission and capacity planning must account for them.
Health and shutdown
/health and /ready return 503 while draining, before configured trading listeners bind, or when session storage/engine recovery/workers fail. An intentionally halted symbol remains operationally healthy; /admin/status reports its actual halted state. Halt/resume state is durable. SIGINT/SIGTERM closes intake and drains connections within the configured timeout; HTTP shutdown is bounded as well. A failed durable write stops further safe processing rather than acknowledging an unpersisted result.
The default log filter is info, including audit events and storage/network warnings. RUST_LOG overrides it; excluding the audit target deliberately suppresses audit telemetry. Asynchronous audit telemetry can drop records under pressure; monitor me_audit_dropped_total and me_audit_disconnected_total. It is not the authoritative durable execution journal. The binary audit representation uses explicit serialization and does not expose struct padding.
This repository supplies a single-node matching service. It does not implement account balances, reserved funds, settlement, replication, consensus, automatic failover, or disaster-recovery orchestration. Those must be provided and tested by the surrounding trading platform. Passing local tests is not evidence of those external guarantees.
Verification
cargo fmt --all --check
cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
cargo test --workspace --all-features --locked
cargo build -p me-server --features tls --locked
ME_TEST_TLS=1 python3 scripts/runtime_smoke.py
python3 -m unittest discover -s examples/python
cargo test --manifest-path examples/rust/Cargo.toml --locked
(cd clients/go/meclient && go test -race ./...)
(cd examples/typescript && npm ci --ignore-scripts && npm test)
cargo audit
Runtime tests start actual processes, exercise TCP and mTLS, kill/restart the server, resume sessions, verify ownership/cancellation and idempotent replies, and check readiness/startup failures. Additional Rust integration tests cover UDS, replay, journal corruption, risk boundaries, and partial-batch recovery.
Monitoring example
Set GRAFANA_ADMIN_PASSWORD to a unique secret, then run docker compose -f monitoring/docker-compose.yml up -d. Grafana is published only on 127.0.0.1:3000; Prometheus is on 127.0.0.1:19090, leaving port 9090 for trading. No default Grafana password is supplied.
The sample scrape target is host.docker.internal:9091. It must reach the engine HTTP listener from the container network: loopback binding on a Linux host is not reachable through the bridge gateway. Bind HTTP to a dedicated private interface reachable only by your monitoring network, or use an authenticated private proxy and update the target. Do not expose the unauthenticated metrics endpoint publicly. Validate up{job="matching-engine"} == 1 after deployment. These rules require an Alertmanager deployment for notifications; the example does not configure one.
Benchmarks
Criterion suites measure the matching engine in isolation. They do not include TLS, network delivery or the durable server's synchronous journal writes.
cargo bench --locked -p me-bench --bench latency
cargo bench --locked -p me-bench --bench throughput
cargo bench --locked -p me-bench --bench e2e
cargo bench --locked -p me-bench --bench stress
cargo bench --locked -p me-bench --bench realistic
Record the Git revision, Rust version, CPU, operating system, compiler options and workload. Run on an otherwise idle machine with a consistent power policy. Criterion writes reports under target/criterion; compare repeated runs rather than a single best value.
me-datagen creates reproducible order streams with --seed. Inspect its options with cargo run --locked -p me-datagen -- --help. Record symbol count, order mix, book depth and seed with any results.
For service measurements, include transport, journal sync, client event application and restart/replay cost. Test both steady load and bursts on the intended filesystem. Report percentile latency, throughput, errors, memory and disk growth. No benchmark number in this repository is a deployment SLA.