← Back to blog

kcore was originally written in Go. That's what most people reach for when building control planes — Kubernetes, Nomad, Terraform providers, they're all Go. It's the safe bet, and I went with it.

Then I rewrote the whole thing in Rust.

That sounds drastic, and it was. But kcore isn't a web service that shuffles JSON around. It writes NixOS system configs, manages TLS certificates, talks to Cloud Hypervisor through Unix sockets, and runs nixos-rebuild as root. The more I worked on it in Go, the more I kept running into problems that Rust just doesn't have. Here's what pushed me to make the switch.

Null doesn't exist, and that's a big deal

This was one of the first things that bugged me in the Go version. Everything can be nil — pointers, interfaces, slices, maps, channels. Forget one check and your service panics at 3 AM. I hit this more than once.

Rust just doesn't have null. If a value might not be there, you use Option<T> — it's either Some(value) or None, and the compiler literally won't let you use it without handling both cases. Same thing for errors: Result<T, E> forces you to deal with the failure path. You can't just ignore it and hope for the best.

In practice, this means a whole category of bugs that would blow up at runtime in Go just don't compile in Rust. For something like a control plane that other services depend on, that's not a nice-to-have — it's table stakes.

The type system actually helps you

Go keeps its type system simple on purpose, and I appreciated that at first. But the further I got into modelling VM states, RPC responses, and configuration variants, the more "simple" started to mean "the compiler lets through mistakes it should have caught."

Rust's enums with associated data are a game changer. You model your VM state as an enum, and if you add a new state later, every single match in the codebase has to handle it or the build breaks. No silent bugs. No forgotten edge cases. The compiler is basically a second pair of eyes that never gets tired and never misses anything.

Fast, and you don't have to think about it

Rust compiles to native code. No garbage collector, no runtime overhead. For kcore, that means snappy gRPC responses, tiny memory footprint on each node agent, and fast Nix config generation even when you're managing dozens of VMs on a single host.

The Go version was fast enough most of the time, but GC pauses showed up in weird places. When your controller is pushing config to nodes and those nodes are applying it live, you want predictable performance. After the rewrite, I just don't think about it anymore — Rust gives you that by default.

This is systems programming — Rust was made for this

Look at what kcore actually does day to day: it writes files to /etc/nixos, spawns subprocesses, reads VM status from API sockets under /run/kcore, manages certificate files with strict permissions. This is low-level stuff.

Rust's ownership model means I don't get data races when the node agent is handling gRPC requests while a background rebuild is running. I don't get use-after-free bugs when passing things between async tasks. Yeah, the borrow checker fights you sometimes — but every time it does, it's catching a real bug that would have been a subtle nightmare to debug in production.

The Nix community already loves Rust

kcore runs on NixOS, and the whole build pipeline is Nix. Here's the thing — the NixOS community has been moving toward Rust for years. Tvix (the Rust reimplementation of the Nix evaluator) is one of the biggest projects in the ecosystem. nil (the Nix LSP) is Rust. nix-ld is Rust. The pattern is clear.

Building kcore in Rust means it fits right in. Nix's Crane library makes Rust builds reproducible without any fuss. People contributing from the NixOS world already know the toolchain. There's no friction between the language and the platform.

When your code runs as root, correctness isn't optional

This is the part that really matters to me. kcore generates NixOS configs and pushes them to nodes where they get applied as root. It writes TLS certificates with specific file permissions. It validates disk paths before handing them to an installer script. If any of this is wrong, you're not just looking at a bug — you're looking at a security incident.

Rust pushes you toward getting these things right. The Nix config generator escapes all user-controlled strings to prevent injection attacks. The auth layer uses exhaustive pattern matching on certificate Common Names. The disk path validator rejects traversal attempts at the type level. These patterns feel natural in Rust because the language was designed to make correct code the path of least resistance.

Honestly, it's just more fun to write

I know this is subjective, but I'm going to say it anyway. Writing Rust is enjoyable. Pattern matching, iterator chains, the trait system — the code ends up reading close to what it actually means. And the tooling is great: cargo clippy is like having a code reviewer that never takes a day off. cargo test, cargo fmt, cargo audit — everything lives under one command. It just works.

When you're building something you're going to maintain for years, the experience of reading and writing the code every day matters more than people think. Rust makes that experience genuinely good.

It's not all sunshine

I'm not going to pretend Rust is perfect. The learning curve is steep — steeper than Go was. Compile times can be painful. The ecosystem is less mature in some areas. And yes, rewriting a working codebase is a big investment. These are real tradeoffs and I won't sugarcoat them.

But for a project like kcore — where the code runs as root, manages real infrastructure, and needs to be reliable above everything else — Rust's tradeoffs are worth it. The compiler catches bugs before they reach production. There's no GC to surprise you. The type system makes sure your invariants hold.

For infrastructure software on bare metal, the rewrite was worth every hour. I wouldn't go back.