Production Deployment
Bare metal, colocation, and why Kubernetes is (mostly) not on the hot path.
Mastery · Markets, Strategies & Getting Hired
Why this week
So far the exchange has lived on your laptop. Production trading systems live somewhere very different: bare-metal servers in an exchange's colocation hall, metres of fibre from the matching engine, on hosts tuned so the hot path never waits for the operating system, released with a discipline that treats every deployment as a risk event. The most expensive deployment bug in trading history is a bad rollout, not a slow one. Knight Capital lost roughly $460 million in about 45 minutes in 2012 after a release reached seven of eight servers.
This week covers how that world works and why its choices differ from the cloud defaults you know. You'll audit a host the way a trading-infrastructure engineer would, plan a rollout that can't take down both halves of a redundant pair, and then ship the demo exchange: a container image, a deploy configuration and a public, read-only market-data feed you can watch live in a browser.
Two kinds of deployment are in play this week. The real kind is bare metal, colocated, kernel-tuned, and changed only outside trading hours. The demo kind is a cheap virtual machine running your container. Learn the first and ship the second. Never quote the second's latency numbers as if they were the first's.
Day 1 — Colocation and the physical layer
Distance is latency
Before any code runs, physics has spent your budget
Light in optical fibre travels at roughly two-thirds of its vacuum speed, about 5 µs per kilometre. Through air, microwave and millimetre-wave links get close to the vacuum speed, about 3.3 µs per km. That gap is why firms built microwave networks between Chicago and the New Jersey data centres: the best fibre routes run around 13 ms round trip, the best wireless routes around 8 ms. Within a building the numbers shrink but the logic doesn't: 100 m of extra cable costs about 500 ns, which is more than a well-tuned matching engine spends on an order.
Hence colocation: you rent rack space in the same data centre as the venue's matching engine and connect to it with a cross-connect. Major venues each sit in one primary data centre (NYSE in Mahwah, Nasdaq in Carteret and CME in Aurora, Illinois, for example), and some equalise the cable length to every colocated customer so that nobody can buy a shorter cross-connect. Inside the rack, latency-sensitive firms use cut-through switches that forward a frame in a few hundred nanoseconds, or layer-1 devices that fan a feed out to many ports in single-digit nanoseconds by not parsing the frame at all.
Starting values are illustrative for a kernel-bypass setup. The point: a budget only helps if every stage has an owner and a number.
Your strategy server is moved to a rack 300 m (of cable) further from the venue's handoff. Roughly how much latency did you add to tick-to-trade, and why is it more than one trip's worth?
You can colocate in one venue's data centre or in a neutral facility that is 0.3 ms from three venues. Which do you choose for (a) a market maker in one product, (b) a cross-venue arbitrage strategy? What does each choice make impossible?
Day 2 — Bare metal, containers and Kubernetes
What a container actually changes
It isn't virtualisation overhead; it's everything around it
A Linux container is a process with namespaces and cgroups. Executing instructions costs the same inside one as outside. The latency problems come from the defaults that surround containers:
- Networking. Default bridge networking puts a veth pair, a bridge and usually NAT rules between your socket and the NIC. Each hop is more kernel work per packet and more jitter.
--network hostremoves them, and kernel-bypass NICs (week 33) need direct device access anyway. - CPU limits. A CPU limit is enforced by CFS bandwidth control. Once a container spends its quota for the period (100 ms by default), its threads are throttled until the next period. A container whose busy-polling thread already uses a full core exhausts the quota early as soon as anything else in it runs (a logger, a GC-like cleanup, a second poller), and then every thread stops for milliseconds. Pin with cpusets (
--cpuset-cpus, or exclusive CPUs from Kubernetes' static CPU manager) instead of capping with quotas. - Neighbours. Containers on a shared host share L3 cache and memory bandwidth. A batch job three cores away can evict your order book. Cache partitioning (Intel RDT/CAT) exists for this, but the usual answer is simply not to share the host.
- Orchestration. A scheduler that reschedules pods, liveness probes that restart a busy engine mid-session, sidecars on your cores and overlay networks are all features in a web stack and incidents on a trading host.
Kubernetes can give a Guaranteed-QoS pod exclusive cores (the static CPU manager policy) and NUMA-aligned resources (the topology manager), and some firms do run latency-tolerant trading components that way. The common split is still: hot path on bare metal, managed by configuration management with pinned threads, and everything else (risk dashboards, reference data, backtests, research) on whatever platform the firm's infrastructure team prefers. Containers on the hot path, when used, are a packaging format: host networking, cpusets, no CPU limits, one per host.
Read the static policy carefully: which pods get exclusive CPUs (Guaranteed QoS, integer CPU requests), what stays on the shared pool, and the options for not sharing an SMT sibling. Then decide whether it would be good enough for your gateway.
A busy-polling feed-handler container has `cpus: 1.0` set as a limit and shows periodic multi-millisecond stalls although the host is mostly idle. Explain the mechanism and the fix.
Day 3 — Host tuning checklist
Make the OS get out of the way
Every item has a number behind it
Low-latency host tuning is a checklist, and every item exists because someone measured a stall:
- CPU governor
performance. On-demand frequency scaling adds ramp-up time after idle. Some firms also fix the turbo state so latency doesn't depend on thermal headroom. - Shallow C-states only (C1 or polling). Waking from a deep C-state takes tens to hundreds of µs (read
/sys/devices/system/cpu/cpu*/cpuidle/state*/latencyon your machine). Set viaintel_idle.max_cstate=,processor.max_cstate=or a PM QoS request on/dev/cpu_dma_latency. isolcpus(or cpusets). Keeps the scheduler from placing other tasks on the cores your pinned threads spin on.nohz_full+rcu_nocbs. Stops the periodic scheduler tick (250–1000 Hz on most kernels) on a core running a single task, and moves RCU callbacks to housekeeping cores.- NIC interrupts off hot cores,
irqbalancestopped. An interrupt on the engine's core costs microseconds and pollutes its caches. Steer IRQs explicitly via/proc/irq/*/smp_affinity. - Hot threads on the NIC's NUMA node. The NIC hangs off one socket's PCIe lanes (see
numa_nodeunder the interface's sysfsdevicedirectory). Remote DRAM is typically ~1.5–2× local latency, and DMA crosses the socket interconnect. - THP
neverormadvise.alwayslets the kernel compact and collapse pages behind your back, with stalls of up to milliseconds. Use explicit huge pages for hot buffers. - Swap off,
mlockall. A page fault on the hot path costs microseconds. Faulting from disk costs milliseconds. - Clocksource
tsc. A vDSOclock_gettimeon the TSC costs ~20 ns. Falling back to HPET makes every timestamp a slow hardware read, roughly a microsecond. - SMT sibling idle, or SMT off. Hyperthreads share L1/L2 and execution ports with your hot thread.
None of this is exotic. Red Hat ships most of it as the latency-performance and network-latency tuned profiles. What matters is that you verify it on every host at every boot: a BIOS update that silently re-enables C6 shows up as a p99 regression weeks later.
The best single page on this topic. Read it with your own core plan in mind: which kernel parameters you would set, and how you would verify each one at boot.
When nohz_full actually stops the tick (one runnable task), what still interrupts a tickless core, and why a housekeeping core is mandatory.
Encode the checklist as code. A checklist in a wiki rots, but an audit that runs at boot and refuses to start the engine on a misconfigured host doesn't.
Host tuning checklist
#[derive(Debug, Clone)]
pub struct Host {
pub governor: &'static str, // cpufreq governor: "performance", "powersave", "schedutil", ...
pub isolcpus: Vec<u32>, // CPUs isolated from the scheduler (kernel cmdline)
pub nohz_full: Vec<u32>, // CPUs running tickless
pub thp: &'static str, // transparent_hugepage/enabled: "always" | "madvise" | "never"
pub swap_enabled: bool,
pub clocksource: &'static str, // current_clocksource: "tsc", "hpet", "acpi_pm", ...
pub max_cstate: u32, // deepest idle state allowed (0 = poll, 1 = C1, 6 = C6, ...)
pub irqbalance: bool, // irqbalance daemon running
pub cpu_node: Vec<u32>, // cpu_node[cpu] = NUMA node of that CPU
pub nic_node: u32, // NUMA node the trading NIC is attached to
pub nic_irq_cpus: Vec<u32>, // CPUs the NIC's queue interrupts are steered to
}
pub struct Plan {
pub hot_cpus: Vec<u32>, // CPUs where pinned hot-path threads will spin
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Finding {
Governor, // governor != "performance"
Thp, // thp == "always"
Swap, // swap enabled
Clocksource, // clocksource != "tsc"
DeepCStates, // max_cstate > 1
IrqBalance, // irqbalance running
NotIsolated(u32), // hot CPU missing from isolcpus
NotTickless(u32), // hot CPU missing from nohz_full
IrqOnHotCpu(u32), // a NIC interrupt is steered to a hot CPU
RemoteNuma(u32), // hot CPU on a different NUMA node than the NIC
}
/// Every finding for this host and plan. Order doesn't matter, but each
/// finding appears exactly once (the plan may list a CPU twice).
pub fn audit(host: &Host, plan: &Plan) -> Vec<Finding> {
let _ = (host, plan);
todo!()
}Write the core plan for a 2-socket, 16-cores-per-socket host running your gateway, risk, engine and market-data publisher with the NIC on socket 0: which core each thread gets, which cores are housekeeping, where IRQs go, and the exact kernel command line.
Day 4 — Releasing without breaking the market
Rollouts are risk events
Canaries, pairs, capacity, and verifying what actually shipped
On 1 August 2012, Knight Capital deployed new order-routing code to eight servers. One server didn't receive it. A configuration flag that the new code reused still triggered long-dead logic on that server, which then sent orders it never tracked as filled. In about 45 minutes the firm made more than 4 million executions and lost roughly $460 million. The SEC's order describes a deployment with no second-person review, no automated check that every server ran the same code, and no fast way to stop. Every one of those has a direct engineering counterpart: release verification, never repurposing flags, and a kill switch (week 15).
How exchanges release:
- Outside trading hours for the matching engine. Many venues change engine code only on weekends, after members have certified against a test environment for weeks.
- Primary/standby swaps for components that must stay up: upgrade the standby, let it replay and match the primary's state hash (week 20), fail over at a quiet moment, then upgrade the old primary. This requires the new version to read the old journal format, which is why week 35 cared about schema evolution.
- Session-aware gateway rollouts: clients keep a primary and a backup gateway, so a drained gateway's sessions reconnect to the backup with sequence numbers intact (week 14). Never drain both halves of a pair at once, and make sure the remaining fleet can absorb the sessions.
- Canary first: one host with the smallest blast radius, watched for long enough to see a full cycle of traffic, before anything else moves.
- Verify what shipped: every host reports its build id, and the rollout fails if any host disagrees.
Canary population size, duration and evaluation metrics. Translate it to a trading fleet: your 'request error rate' is reject rate, session drops and order-to-ack latency per gateway.
Read the findings section as a postmortem. List every control that would have stopped it, and whether your own exchange has it.
Name three engineering controls that would each independently have prevented or limited the Knight Capital loss.
Now plan a gateway rollout under those constraints. Waves drain in parallel, pairs never drain together, the fleet must absorb every drained session, and the canary goes first.
Plan a zero-downtime rollout
#[derive(Debug, Clone)]
pub struct Host {
pub id: u32,
pub sessions: u32, // client sessions currently on this gateway
pub capacity: u32, // max sessions it can hold
}
pub struct Fleet {
pub hosts: Vec<Host>,
/// (a, b): clients use a and b as primary/backup — never drain both at once.
pub pairs: Vec<(u32, u32)>,
pub max_per_wave: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PlanError {
NoHosts,
CannotDrain(u32),
}
/// Return the waves, in order, as lists of host ids.
/// 1. Wave 0 is a single canary: the host with the fewest sessions
/// (ties → lowest id).
/// 2. Every wave has 1..=max_per_wave hosts; every host is in exactly one wave.
/// 3. No wave contains both hosts of a pair.
/// 4. While a wave drains, its total sessions must fit in the spare capacity
/// (capacity − sessions) of all hosts NOT in that wave.
/// 5. If a host cannot drain even alone, return Err(CannotDrain(id)) for the
/// lowest such id. Empty fleet → Err(NoHosts).
/// Fewer waves is better; any plan satisfying 1–4 is accepted.
pub fn plan_rollout(fleet: &Fleet) -> Result<Vec<Vec<u32>>, PlanError> {
let _ = fleet;
todo!()
}
Write the step-by-step upgrade of your engine's primary/standby pair during a trading week: what you verify before each step, the exact condition for failover, the rollback at each step, and what you tell members beforehand.
Day 5 — The demo, deployed
A public exchange you can point at
Small, honest, and safe to expose
The demo is your exchange in one process: the engine, simulated order flow (sim::LiveSession, the same provided flow that the WebAssembly page and the native terminal run) and the WebSocket market-data feed (week 16). It runs on a small virtual machine anyone can connect to. It isn't a latency showcase. A shared VM has noisy neighbours, steal time and no kernel tuning. It's proof that the system runs, recovers and behaves, and it's something to watch.
Packaging choices that matter:
- Build once, promote the artifact. A multi-stage container build with your week 22 release profile produces the image, and the same image digest goes to every environment. If you build fully static against musl, swap in a faster allocator: musl's default
mallocis much slower than glibc's under allocation-heavy load. - Configuration from the environment or a mounted file, validated at startup by week 22's rules. Bad config should stop the process with a readable error, not fall back to a silent default. The repo's demo has its own small
demo::Config(it doesn't use the gateway'sexchange.toml): defaults, thenHFT_DEMO_*variables, then flags, parsed once at startup. Bad input exits with status 2 and a message naming the setting and where it came from. - Graceful shutdown on
SIGTERM: stop accepting connections, flush the journal, close WebSocket sessions, exit inside a deadline (week 3). Container platforms sendSIGTERMand thenSIGKILLafter a grace period, and your drain has to fit inside it. The repo's demo installs no signal handler (std has none), soSIGTERMends it at once. That's a clean stop only because the demo keeps no journal. Run it under an init (docker run --init, or the platform's own) so the signal reaches it at all. Its graceful paths are--duration, a token-guardedPOST /shutdownandDemo::shutdown. - Expose only what's safe. The public endpoint is read-only market data. Cap connections, limit per-IP subscription rates (week 17), give every client a bounded buffer with conflation (week 16), and terminate TLS in a reverse proxy so browsers can use
wss://. Keep order entry and metrics off the public interface. The repo's demo has no order entry at all, refuses WebSocket clients beyond--max-ws-clientswith a 503, and leaves TLS to the platform's proxy. It has no per-IP limits, and it serves/metricson the same port as the page;fly.tomlnotes where to move it in a real deployment.
Your deployed demo's /metrics shows a p99 publish latency of 180 µs; the week 40 report said under 1 µs for the engine. Is something broken?
Day 6 — Operating it
Days have a shape
Start of day, end of day, and the runbook in between
Exchanges run on a schedule: start-of-day checks, a pre-open phase that accepts orders without matching, an opening auction, continuous trading, a closing auction, end-of-day processing, and often a nightly restart that resets sessions and sequence numbers. Operations means every phase transition is scripted, observed and reversible. Your demo should have the same shape in miniature: a start-of-day check (config valid, journal readable, host audit clean), a published session schedule, and an end-of-day snapshot.
Write the start-of-day and end-of-day checklists for your demo exchange as commands with expected outputs. Which failures block opening, which only page someone, and which are logged for the morning?
Capstone
Build crates/demo into the deployable demo: one binary, demo, that runs the engine with a seeded simulated market (sim::LiveSession) at a fixed rate, publishes L2 updates through week 16's broadcast ring to a WebSocket feed, and serves a small HTTP API, plus the packaging around it. Config (flags and HFT_DEMO_* variables), Stats, the page and the test clients are provided. You write Demo and the server wiring in server.rs: the engine thread, the feed's accept thread with one thread per client, and the HTTP server. Its routes are / (the page), /health, /metrics (Prometheus text), /stats (JSON) and POST /shutdown, which exists only when a shutdown token is configured. demo healthcheck probes /health, for container health checks in an image without curl. The packaging is Dockerfile.demo at the repo root (multi-stage, release build, distroless glibc runtime, non-root user, a HEALTHCHECK that runs demo healthcheck) and fly.toml, an example Fly.io deployment that nothing deploys automatically. Use the platform you prefer; a compose file or a systemd unit works as well.
The health endpoint must report readiness only when the demo can serve: /health answers 200 once the feed has published and 503 while starting or stopping. A demo with a journal would also wait for its replay to finish; this one keeps none, so a publishing feed is the whole condition.
Decisions to make deliberately. Fix the simulation seed by default so the demo is reproducible, and make it overridable. The repo's default is 42, and a test checks that the feed rebuilds exactly the book an offline replay of the same seed has. Bound every per-client queue and decide your slow-consumer policy (conflate or disconnect) before the first browser tab stalls your publisher. In the repo the engine never waits: a slow client lags, gets conflated batches or a fresh snapshot, and is disconnected if its socket stays full. Make shutdown a tested path, not an afterthought. The repo's tests stop the demo with POST /shutdown (a wrong token gets 403; the right one gets 202, the demo drains within the deadline and every feed client is closed) and run the binary with --duration 2s, checking that it exits by itself and prints its summary. A demo that keeps a journal also needs a test that sends SIGTERM and checks the journal was flushed, and that means installing a signal handler. Keep the public surface read-only.
Watch it live. Open the deployed demo's page at /. It is a client of the feed like any other: it draws the depth ladder from the WebSocket updates, coalesced to the display's frame rate rather than rendering every message, and the trade tape from /stats. The feed's real consumer test is in the tests: w47_feed_caps_clients_and_never_waits_for_slow_ones connects a client that never reads and checks that the engine keeps its pace and the other clients keep receiving.
The hft-exchange repo also includes a native trading terminal in apps/terminal, built with GPUI (the GPU-accelerated Rust UI framework that powers the Zed editor) and the gpui-component library. It doesn't connect to the demo. It runs its own in-process LiveSession (your engine with the same simulated flow) and shows the depth ladder, candles, depth and a live per-command latency histogram of your engine, with order entry so you can trade against the flow. Use it to watch your own engine live; use the demo's page to watch the deployment.
Pitfalls. Health checks that only test "process is up" will route traffic to an engine still replaying. Containers that run as root with host networking on a public VM are an invitation. Logs written synchronously from the hot path will dominate your latency on a cheap disk. And a demo that needs you to SSH in after every reboot is not deployed: it should come back by itself.
crates/demoShip the demo exchange
One command builds the image; the deployed demo streams live market data you can watch on its own page.
cargo xtask test --week 47 # tests named w47_*
cargo xtask bench --week 47 # p50 / p99 / p99.9 on your machine
cargo xtask report --upload # update this page and your lab- All week 47 tests pass w47_*
The component library the terminal is built on: tables, lists, layout and theming on top of GPUI. GPUI is still pre-1.0, so pin versions and expect API churn when you upgrade.
Feeds into
- Week 51 turns this week's commands and deploy configuration into the README's "run the demo" section, and its tests hold that section to the binary: every flag and
HFT_DEMO_*variable it names must exist indemo --help, and it must point atDockerfile.demoandfly.toml. Link the deployed demo from there.