FIX Lab
Two real FIX 4.4 engines and a matching engine between them. Log on and this site’s server starts a FIX session of your own — your SenderCompID, your sequence numbers — against an exchange running in the same server, then shows every message on that session’s wire, both ways, with every tag named. Send limit orders into a shared order book, lose a message on purpose and watch the engines recover it, or go silent and watch the exchange give up on you.
The instruments are fictional: ACORN.LAB, BIRCH.LAB, with made-up prices. Orders on this page go only into this page’s own book, shared by whoever has it open, and nothing here reads or mirrors any market.
Your session
Connecting to this page’s server…
| Next MsgSeqNum it sends | Next MsgSeqNum it expects | |
|---|---|---|
| Your session (—) | — | — |
| The exchange (QuickFIX/Go acceptor) | — | — |
Break it on purpose
Drop next outbound message. A tap between your engine and the exchange withholds the next message your session sends. Your engine has already used its sequence number; the exchange never sees it. Send another order (or wait up to 6 seconds for your next Heartbeat) and that message arrives one number too high: the exchange’s engine sends a ResendRequest, and yours resends the lost message with PossDupFlag=Y — or, if it was a Heartbeat, a SequenceReset gap fill.
Go silent. The tap withholds everything your session sends from then on. After 7.2 seconds with nothing from you the exchange sends a TestRequest; your engine answers it, but the answer is withheld too; after 14.4 seconds the exchange’s engine ends the session. QuickFIX/Go disconnects at that point without a Logout message, and the exchange cancels anything you had resting.
Send a limit order
- ACORN.LAB, Acorn (fictional): reference 100.00; the exchange takes prices from 80.00 to 120.00. A price from 99.51 to 100.49 rests inside the bots’ spread, so another reader can trade with it.
- BIRCH.LAB, Birch (fictional): reference 25.00; the exchange takes prices from 20.00 to 30.00. A price from 24.89 to 25.11 rests inside the bots’ spread, so another reader can trade with it.
Order books
Shared by everyone on this page, plus 2 liquidity bots that are ordinary FIX sessions, each resting 100 at 2 prices a side and putting an order back when it fills. Best 6 prices a side; price-time priority, each trade at the resting order’s price.
| Bid qty | Bid | Ask | Ask qty |
|---|
| Bid qty | Bid | Ask | Ask qty |
|---|
Your orders and fills
Built only from the ExecutionReports your session received.
| ClOrdID | Symbol | Side | Price | Qty | Filled | Avg price | Status | Cancel |
|---|
| Match ID | ClOrdID | Symbol | Side | LastQty | LastPx | Leaves |
|---|
Your session’s wire
Every message on your session, newest first, as the tap between the two engines saw it. The field
delimiter (byte 0x01, SOH) is shown as |. Open a message to see each tag by its FIX 4.4
name. A message marked withheld was sent by your engine and never reached the exchange.
Benchmark
The same exchange and the same engine, run with docker compose
from the public repository
at v0.1.0, read from the
committed results.
Machine: Intel(R) Core(TM) i7-8650U CPU @ 1.90GHz, 8 logical CPUs, 61 GiB of memory, Linux 7.0.0-38-generic, go1.27.2, QuickFIX/Go v0.9.12. Date: .
One exchange container and one bench container on the same machine, talking FIX 4.4 over TCP on the docker compose network. Each run starts a fresh exchange. Orders alternate buy and sell, one unit each, at one price on one fictional instrument, so they trade with each other and the book stays small; every order gets an ExecutionReport New and most also get a fill.
| Run | Sessions | In flight per session | Orders | Acknowledged | Rejected | Orders / s | Ack p50 | Ack p90 | Ack p99 | Slowest |
|---|---|---|---|---|---|---|---|---|---|---|
| latency | 1 | 1 | 20,000 | 20,000 | 0 | 2,699 | 333 µs | 438 µs | 989 µs | 7.4 ms |
| concurrent | 4 | 8 | 100,000 | 100,000 | 0 | 4,523 | 6.0 ms | 11 ms | 21 ms | 131 ms |
| throughput | 8 | 32 | 200,000 | 200,000 | 0 | 720 | 277 ms | 645 ms | 1.01 s | 1.88 s |
- Throughput does not grow with sessions here: it falls from 4,523 orders/s at 4 sessions to 720 at 8. A CPU profile of the same load in one process (8 sessions, window 4) put 64% of CPU in runtime.selectgo under QuickFIX/Go's stateMachine.SendAppMessages. In v0.9.12, when a session's socket writer is busy, session.sendQueued(false) cannot hand off a queued message and calls notifyMessageOut(), which re-signals the same session loop at once, so each session with something queued polls until its writer is free. At 8 sessions there are at least 16 such loops (both ends) on 8 CPUs, and they take the CPU the writers need. This is the engine as published; this repository does not patch it.
- The latency figures are a client-side round trip on one clock, through the engine, the kernel's TCP stack and the compose bridge, and back. They are not a one-way figure and do not include any network beyond this machine.
- The exchange keeps every message it sends in QuickFIX/Go's in-memory store for the life of the session, as the lab does, so memory grows with the run length.
Method
What FIX is. The Financial Information eXchange protocol
is the message format most order flow between financial firms uses. A message is a list of
tag=value fields separated by the SOH byte: tag 35 says what kind of message it is, 34 is
its sequence number, 49 and 56 say who sent it to whom. Two kinds of message share one connection: the
session layer (Logon, Heartbeat, TestRequest, ResendRequest, SequenceReset, Logout), which keeps
the connection honest, and the application layer (NewOrderSingle, OrderCancelRequest,
ExecutionReport, OrderCancelReject), which carries the business. Every message each side sends is
numbered, and each side stores what it sent, so a gap can be detected and filled.
Two real engines. Both ends are
QuickFIX/Go
v0.9.12, used as published. When you log on, this server starts an
initiator session with a fresh SenderCompID for you and connects it, over this server’s own loopback
interface, to an acceptor running in the same process. Your browser never speaks FIX: it sends small
commands over this site’s WebSocket, and receives a copy of every message on your session’s
wire. Heartbeats, TestRequests, resends, gap fills and the end of a session are the engines’ own
behavior; nothing on this page or in this server writes or edits a FIX message on a session’s behalf.
The engine code is imported from the
public repository, where the same
exchange runs in its own container and a client in another, over TCP.
The matching engine. Each instrument has one limit order book. An incoming order trades first against the other side, best price first and, at one price, oldest first; each trade happens at the resting order’s price; whatever is left rests at its own price behind the orders already there. Both sides of a trade receive an ExecutionReport with ExecType F and the same match ID. Only limit orders are accepted, within 20% of each instrument’s reference price, from 1 to 1,000 units; anything else is answered with a rejected ExecutionReport saying why. A cancel for an order that is no longer open is answered with an OrderCancelReject.
What it does not do. No money, no accounts and no market data are involved. The books hold only what this page’s visitors and its bots send them; the prices mean nothing outside it. There are no market orders, no stops, no order amendments and no self-trade prevention: two of your own orders can trade with each other.
- One FIX session per open page; 2 pages per address and 20 across the site at once. A session ends after 10 minutes, with a Logout, and you can log on again.
- Heartbeat interval 6 seconds, so a silent session ends within about 14.4 seconds.
- Up to 5 orders at once and 2 per second after that, and 20 open orders per session.
- Everything is in memory: the books, and each session’s stored messages. When a session ends its resting orders are canceled and its messages are gone. Nothing about you is stored.
Why 20 sessions: this site runs on one virtual CPU, and the benchmark above found that QuickFIX/Go’s session loop polls while its socket writer is busy, so throughput fell as more sessions were busy at once. Orders sent at a person’s pace keep each session nearly idle; the cap keeps the total far from that point.