Toolchain Horizons: Exploring Rust Dependency-Toolchain Compatibility
Systems Distributed '26 in Boston
Get Tickets ↓
I tested the top 100 Rust crates on crates.io for backwards<br>compatibility. Then I removed every dependency from the TigerBeetle Rust<br>client and backported it to Rust 1.39, from 2019.
Here’s why: last year I created the Rust client for TigerBeetle, the<br>financial accounting database. One morning, after weeks of work reducing<br>the client’s minimum supported Rust version, I found CI broken — a point<br>release of a dependency of a dependency had bumped its rust-version<br>from 1.61 to 1.68.
This annoyed me a lot, so I ran an experiment.
TigerBeetle clients
TigerBeetle has a client-server architecture, and provides client<br>libraries for most popular languages: Python, Java, Go, Node, .NET, and<br>Rust. Each of these is a bindings/FFI project that binds to the single<br>tb_client library, written in Zig, exposing a C ABI,<br>wrapped in the idioms of the embedding language. We share one core<br>across every language client so that as much of the client-side code<br>path as possible is covered by our deterministic simulation testing (the<br>VOPR).
TigerBeetle client architecture: language<br>clients (Python, Java, Go, Node, .NET, Rust) all connect to the central<br>tb_client library written in Zig
The Rust client is less than two thousand lines of production Rust<br>code, some of it generated, some of it written by hand. It provides a<br>simple asynchronous API for use with async /<br>await; and it is runtime-agnostic, requiring no<br>dependencies on specific Rust async runtimes.
In use it looks like this:
use tigerbeetle as tb;
// Connect to TigerBeetle<br>let client = tb::Client::new(0, "127.0.0.1:3000")?;
// Create two accounts on the same ledger<br>let accounts = [<br>tb::Account {<br>id: tb::id(),<br>ledger: 1,<br>code: 1,<br>..Default::default()<br>},<br>tb::Account {<br>id: tb::id(),<br>ledger: 1,<br>code: 1,<br>..Default::default()<br>},<br>];<br>client.create_accounts(&accounts).await?;
// Transfer 100 units from the first account to the second<br>let transfers = [tb::Transfer {<br>id: tb::id(),<br>debit_account_id: accounts[0].id,<br>credit_account_id: accounts[1].id,<br>amount: 100,<br>ledger: 1,<br>code: 1,<br>..Default::default()<br>}];<br>client.create_transfers(&transfers).await?;
It’s a small codebase with basic requirements but it does need some<br>common dependencies.
Minimum Supported Rust Versions
The Rust ecosystem has a concept of the “Minimum Supported Rust<br>Version” (MSRV) for its crates. It relates crates to the Rust compiler<br>version, and it is separate from SemVer, Rust’s primary versioning<br>scheme.
Some crates encode this information in their Cargo.toml<br>manifest’s optional rust-version<br>field. It is a best practice for crate maintainers to know and document<br>their minimum supported Rust version and test against that version in<br>their CI.
When I do the initial development of a new Rust crate, I don’t worry<br>about the minimum Rust version; I save that work for just before<br>publication, a process like:
Start with the oldest version I know I support.
Test against the previous version.
Fix the build.
This usually involves removing or replacing dependencies, and<br>replacing newer language features with older ones or dependencies that<br>fill that role. Open-coded polyfills are often involved.
Update CI to verify that as the minimum supported Rust<br>version.
Do it again.
When I posted the initial pull<br>request for the Rust TigerBeetle client in June 2025, I had not done<br>this yet, and expected our minimum supported Rust version to be a recent<br>one. Without any effort to support older releases, the client’s initial<br>MSRV was Rust 1.81, published September 5, 2024.
About 9 months of supported toolchains. Not satisfactory, but not<br>surprising.
TigerStyle and dependencies
TigerBeetle has a set of strict and opinionated coding guidelines, TigerStyle.<br>They are focused on three pillars: safety, performance, and developer<br>UX.
TigerStyle emphasizes fully understanding and owning your code and<br>radically reducing dependencies. It has this to say about them:
TigerBeetle has a “zero dependencies” policy, apart from the Zig<br>toolchain. Dependencies, in general, inevitably lead to supply chain<br>attacks, safety and performance risk, and slow install times. For<br>foundational infrastructure in particular, the cost of any dependency is<br>further amplified throughout the rest of the stack.
In order to support older Rust toolchains — and as a matter of<br>TigerStyle — one of my first tasks to land the Rust client was to<br>judiciously remove crate dependencies.
At the start of the process the client’s dependencies were thus:
[package]<br>name = "tigerbeetle"<br>version = "0.1.0"<br>edition = "2021"
[dependencies]<br>bitflags = "2.6.0"<br>futures = "0.3.31"<br>thiserror = "2.0.3"
[build-dependencies]<br>anyhow = "1.0.93"<br>ignore = "0.4.23"
[dev-dependencies]<br>anyhow = "1.0.93"<br>tempfile = "3.15.0"
To Rust programmers this is common stuff, dependencies most of us<br>use. Most of these are easy enough to remove.
The Rust futures...