← All articles

No. 08

A microsecond is 200 metres of fibre

Before Nexow Studio, I co-founded Nexow Systems, a trading firm in Barcelona. What studying high- and ultra-high-frequency trading taught me about latency, and why its fast path is written in C and, now, in Rust.

Alex Colls Outumuro

Co-founder and CTO · · 6 min read

Print 08, Feather. Drops and combs on a bath, pulled when the site is built.Point at it to drop ink.

Nexow Studio is not the first company to carry the name. From 2015 to 2020, I co-founded and ran Nexow Systems, a trading firm in Barcelona. Its website described it as a private investment company specialised in multi-asset quantitative trading, low-latency algorithms and market making. When we closed it, I kept the brand and my algorithms. This is what the fast end of the market taught me.

Fast, and faster

High-frequency trading is less a strategy than a property of some strategies: their edge decays with time. A price that is wrong in one place is corrected a few milliseconds later, and whoever acts first takes the difference. Market makers quote both sides of a book and earn the spread; arbitrageurs buy in one place and sell in another. Each trade earns very little, so it only pays at volume, and only for those who arrive first.

Ultra-high frequency is the end of that scale where the race is decided in microseconds, measured wire to wire: from the moment a packet carrying a price reaches the network card to the moment an order leaves it. There, ordinary software is already too slow. Firms put their servers in the exchange’s own data centre, bypass the operating system’s network stack, and move the decision itself into FPGAs.

Our own book sat mostly at the slower end. Of the 53 strategies on our 2020 sheet, the largest group, 27, were mid-term: an average holding under five days. Eight were labelled ultra-short term, under an hour on average, and seven of those never left the idea stage. For a UHFT desk, an hour is geological time.

Latency is a distance

The first lesson is physics. In a vacuum, light covers about 300 metres in a microsecond. In optical fibre, whose glass has a refractive index of about 1.47, it covers about 204. No code makes that faster: every microsecond spent on distance is a microsecond the code no longer has.

t = d × n / c

1 km of fibre (n ≈ 1.47)            4.9 µs
Aurora → New Jersey, ≈ 1,180 km
  in a vacuum (n = 1)               3.9 ms
  in fibre (n ≈ 1.47)               5.8 ms

Those are straight lines; real cables are longer. And those two places are the textbook case. The E-mini S&P 500 future, ES, trades at the CME, whose matching engine sits in Aurora, Illinois. SPY, a fund that tracks the same index, trades on exchanges in New Jersey. Over a minute or a day, the two move almost as one. Over milliseconds, the correlation breaks down: in The High-Frequency Trading Arms Race (2015), Budish, Cramton and Shim measured that the median ES/SPY arbitrage opportunity lasted 97 ms in 2005 and 7 ms in 2011, while its profitability stayed the same. The window shrank to the length of the cable.

A study in C

Among the files I kept from those years is an experiment Johann Christian Lotter published in 2017 on his blog, The Financial Hacker: an ES/SPY arbitrage system for Zorro, the backtesting platform our quants worked in, run over one day of tick data, 5 October 2016: CME quotes for ES and Nanex’s NxCore feed for SPY. Zorro scripts are written in lite-C, a dialect of C, and the whole decision fits in one function. SPY is worth a tenth of the index, so its prices are multiplied by ten before they get there.

int tradeHFT(double AskSPY,double BidSPY,double AskES,double BidES)
{
  double SpreadSPY = AskSPY-BidSPY, SpreadES = AskES-BidES;
  double Arbitrage = (AskSPY+BidSPY-AskES-BidES)/2;
  static double ArbMean = Arbitrage;
  ArbMean = 0.999*ArbMean + 0.001*Arbitrage;
  static double Deviation = 0;
  Deviation = 0.75*Deviation + 0.25*(Arbitrage - ArbMean);
  static int Position = 0;
  if(Position == 0) {
    if(Deviation > SpreadSPY+THRESHOLD) return Position = 1;
    if(-Deviation > SpreadES+THRESHOLD) return Position = 2;
  } else {
    …

Read it as signal processing. ArbMean is a slow exponential moving average of the gap between the two prices, with α = 0.001; Deviation is a fast one, α = 0.25, of how far the gap strays from it. When it strays further than the spread you would pay plus a threshold, the system buys the cheap side and sells the dear one. Nothing in it is clever. The published result is the lesson: over that day, with 0.5 ms of latency the backtest made $793; with 4 ms, $273; with 6 ms, $205; with 10 ms, it lost $15.

Same code, same day. Only the distance changed.

Why C, and why Rust

Once the distance is paid, what is left is your own code, and there the enemy is not the average but the tail. A system that usually answers in microseconds and now and then in milliseconds gets picked off in exactly those moments, and they do not fall at random: a burst of market data is what fills the queues and triggers the pauses, and a burst is when prices move. Garbage collectors pause. System calls, page faults and locks held by another thread pause. C lets you remove all of them: memory laid out by hand and allocated before the open, threads pinned to cores that spin instead of sleeping.

At that scale the hardware is the data structure. A cache line on x86 is 64 bytes, and two threads writing different variables on the same line fight over it. Prices arrive as integers and should stay integers, because binary floating point cannot even hold 0.1 exactly. The CME lines in that experiment come that way: 215800 with a display factor of 0.01, which is 2,158.00 index points.

In Rust, a quote can carry all of that in its type. This is a sketch, not code from one of our products:

#[repr(C, align(64))]
pub struct Quote {
    pub ts_ns: u64,
    pub bid: i64,
    pub ask: i64,
    pub bid_qty: u32,
    pub ask_qty: u32,
}

Rust keeps that control. There is no garbage collector, nothing is allocated unless you ask, and #[repr(C, align(64))] fixes the layout: the struct above holds 32 bytes of data and takes a cache line of its own. What Rust adds is a compiler that refuses the bug hardest to find in C, two threads touching the same memory without saying so. In C that compiles, runs, and may surface much later as a wrong price. In Rust a value only crosses threads if its type is Send, is only shared if it is Sync, and in safe Rust a data race is a compile error.

What comes next

Rust came after Nexow Systems. I opened the nexow repository in 2023 and wrote its trading engine there in 2025, in the open: the engine runs on its own thread and streams bars, orders and metrics over crossbeam channels to an Axum server, with a Nuxt station on top. Today it is an MVP that trades synthetic bars with a random forest. It is not an HFT system yet, and I will not pretend otherwise: its channels are unbounded, so they allocate as they grow, and on a hot path that becomes a ring buffer sized once, before the open.

Nexow Studio is not a trading firm, but the lessons stayed with the name. Count the distance before the code. Measure the tail, not the mean. And keep integers integers.

Alex