Why we decided to write our real-time market gateway in Rust

April 03, 2023
Title picture for Why we decided to write our real-time market gateway in Rust

In Databento's live market data architecture, our real-time subscription gateway is one of the most critical components. The gateway is responsible for serving normalized market data to our clients and managing all the connections incoming from our client libraries, as well as our raw market data API. When designing a new component, the choice of programming language and environment are usually the first things developers decide on. Here are some reasons why we decided to go with Rust to serve our market data.

Rust live gateway 5c741d48a1 webp

Our Live Subscription Gateway is one of the main points of entry for customers to Databento's infrastructure. Because of this, it's very exposed to potential attempts of malicious clients to exploit software vulnerabilities. Some of the most exploited attack vectors are memory vulnerabilities, and for an application exposing a network endpoint, buffer overflows are an ever-present danger.

While Rust doesn't solve every possible memory access vulnerability, it isolates the portion of the code which could have these issues to unsafe blocks. This allows us to write safe abstractions around unsafe code - so that we can optimize our efforts by auditing these abstractions more thoroughly.

The trading industry is somewhat unique when considering latency requirements for applications. There are use cases, such as click-trading, where latency isn't very important (because the bottleneck is human reaction speed). Some automatic use cases don't demand very low latency because of the nature of the market. In markets with last-look mechanisms, for example, the quote issuer has ample time to reject a transaction going against them. In other cases, such as simple arbitrage, market participants can have a tick-to-trade latency of less than 100 nanoseconds.

Here at Databento, we try to create a middle-of-the-road solution that can benefit as many customers as possible. We're using TCP as our transport layer protocol and distributing over the internet (we were aiming for latency within 20 microseconds on our initial goals). For that kind of latency, languages with garbage collection can be prejudicial—a GC-induced stop can suddenly create huge spikes in latency, especially in the busiest moments!

Not wanting a runtime with a garbage collector, C, C++, and Rust stand out as the main mature options we could use. These three are largely comparable in terms of performance. In our use cases, Rust code performance has shown to be adequate.

Since we're serving our clients via internet over TCP, our gateway needs to be able to cope with potential slowness on the client side, without punishing clients who can't keep up with the load - not everyone has the same internet connection!

Asynchronous I/O is a convenient way of ensuring maximum throughput of our application—if one client is too slow in acknowledging their packets, it won't be serviced as often to give them time to catch up. While the I/O is happening, our application threads can serve other clients, allowing us to scale better with the number of incoming connections.

Rust has the Tokio runtime, which allows us to program asynchronously almost in the same way that we would write synchronous code! While it's possible to write asynchronous code in C and C++ (using for example libuv or Boost.Asio), asynchronous programming is far from being a first-class citizen.

One of the most common sources of bugs in multithreaded applications is the improper sharing of resources. Rust's ownership model ensures that a single piece of data is only being written from one place at a time, and is not being read while it's written. This ownership model is enforced by the compiler - code that naively tries sharing mutable references to the same data across threads, for example, will not compile.

These assurances allow us to iterate much more quickly without having to look over our shoulders constantly for potential data-sharing issues. This synergizes well with the Tokio runtime—at the same time that we have the convenience of writing asynchronous code easily, we are also protected from a wide range of bugs.

We also have some of our own low-level primitives for data-sharing at Databento, and these are mostly written in C. Rust's FFI support makes it easy to integrate those into the codebase.

The factors exposed here are by no means exhaustive, but we hope that they show some ways in which adopting Rust can lead to better software in the financial domain! If you want to get started with Databento and you also love Rust, check out our DBN crate here.