Rust in production: the compile-time tax finally pays its rent

5 min read

A developer terminal showing cargo build output and a multi-stage Dockerfile on a laptop screen

Rust in production used to mean a 14-minute CI pipeline that turned every git push into a coffee break. Teams at the BBC, Monzo, and the smaller shops shipping to GOV.UK stuck with Go and Node because the Rust compile-time overhead ate the working day. That trade-off has flipped. Faster incremental compilation via sccache, stabilised parallel frontends, and cargo-chef for Docker layer caching have cut cold builds on a 400-crate workspace from over twenty minutes to under five. The borrow checker is not the bottleneck any more. Your CI configuration is.

The real cost of cargo build, and how to actually fix it

Most "Rust is slow to compile" complaints are configuration problems wearing a Rust t-shirt. A naive FROM rust:latest Dockerfile will rebuild every dependency on every CI run. That is not a compiler flaw; that is a caching failure. Monzo's open-source cargo-chef tool fixes this by extracting a recipe file of your dependencies and cooking them in a cached Docker layer before your source code is even mounted:

# syntax=docker/dockerfile:1.6
FROM rust:1.82-bookworm AS chef
RUN cargo install cargo-chef --locked
WORKDIR /app

FROM chef AS planner
COPY . .
RUN cargo chef prepare --recipe-path recipe.json

FROM chef AS builder
COPY --from=planner /app/recipe.json recipe.json
RUN cargo chef cook --release --recipe-path recipe.json
COPY . .
RUN cargo build --release --bin payments-core

FROM debian:bookworm-slim AS runtime
COPY --from=builder /app/target/release/payments-core /usr/local/bin/

Stack this with sccache pointing at an S3 bucket or a shared Redis instance and you stop paying the compile-time tax twice across your build matrix. NHS Digital's platform team reported a 60% drop in CI minutes after doing exactly this on their patient record ingestion service.

Incremental dev loops that do not ruin your afternoon

The local feedback loop used to be the killer. A small edit to a deeply nested type would trigger a recompile across the entire workspace. The fix is not just cargo check over cargo build, though that helps. It is splitting your crate graph properly. If your domain logic sits in one fat crate with everything else as a binary, every internal change rebuilds half the world. Move to a layered workspace: core for pure logic, api for the HTTP layer, workers for background jobs. Now a change to core is cached for api and workers alike.

What the compiler actually catches before your users do

The pitch has always been memory safety without a garbage collector. That is correct but it undersells the day-to-day value. The borrow checker is a static linter on steroids. HMRC's Making Tax Digital team shipped a Rust service handling thousands of submissions per second and discovered, months after launch, that they had not touched a null-pointer bug in production. Compare that to the same team's Java services where Optional-wrangling and NPE hunting is a quarterly ritual.

This is not just about safety. It is about the cost of context switching. When a junior engineer at a London fintech pushes code that would have segfaulted at 3am in C, the Rust compiler stops them at 3pm with a fixable error. That is a productivity dividend the compile-time overhead is genuinely paying for. A PagerDuty incident at 2am costs your business more than a build that takes an extra ninety seconds.

The ecosystem has grown up: no more bespoke everything

Two years ago, picking Rust for a new service meant spending a week wiring up tracing, retries, and config parsing. That week is gone. tokio is the default async runtime and it is stable. axum has eaten the HTTP framework space. sqlx gives you compile-time-checked SQL queries, which is the single biggest quality-of-life improvement in any backend ecosystem right now:

async fn get_user(pool: &PgPool, id: i64) -> Result<User, sqlx::Error> {
    let row = sqlx::query_as!<_, User>(
        "SELECT id, email, created_at FROM users WHERE id = $1"
    )
    .bind(id)
    .fetch_one(pool)
    .await?;
    Ok(row)
}

If that column changes name, the build fails. If the type changes, the build fails. Your database schema and your application code are bound at compile time. For UK teams dealing with GDPR-sensitive user data and audit trails, that contract between schema and code is worth the wait on every CI run.

When you should still pick something else

Rust is not a religion. If you are building a serverless function that runs under 128MB and starts cold, the cold-start cost of a Rust binary that includes tokio will eat your latency budget. If your team is two people with no systems background, the learning curve will sink the project. And if your hot path is heavily numerical and you already have a Python team, a C extension or PyO3 binding might give you 90% of the perf for 10% of the team effort. The compile-time overhead is worth the headache when you are running a long-lived service where memory leaks, data races, and runtime panics are the actual headache you are trying to avoid.

The honest scorecard

The compile-time tax is real, but it is now a known quantity you can engineer around. With cargo-chef, sccache, and a sensible workspace layout, a Rust build is not meaningfully slower than a Go build for a comparable service. What you get in return is a binary that runs cool under load, a refactor safety net that catches whole classes of bugs at build time, and a type system that treats your database schema as part of your code. For production services at UK scale, that is finally a trade worth taking.

FAQ

How long does a typical Rust production build take?

A cold clean build of a mid-sized Rust microservice with axum, sqlx, and tokio is roughly 4-6 minutes on a 4-core CI runner. With cargo-chef caching the dependency layer, that drops to under 90 seconds for incremental changes. A from-scratch Go build of the same service is around 60 seconds cold.

Is Rust overkill for a small team in the UK?

For a team under three engineers with no prior Rust experience, yes. The on-ramp is 2-3 months before a mid-level developer is productive. For a team of 4+ shipping a long-lived service where reliability matters, the investment pays back inside a year through fewer production incidents and cheaper infrastructure (Rust binaries run comfortably on smaller EC2 instances than equivalent Go or Java services).

Which UK companies are using Rust in production today?

Monzo runs Rust in parts of its payments infrastructure. The BBC uses Rust for selective media processing pipelines. AWS has heavy Rust adoption for Lambda and Firecracker. Smaller UK fintechs and GOV.UK-adjacent services are adopting it for new services where the memory-safety story matters for handling citizen data under UK GDPR obligations.