Migrations

Migrating hot paths from Python & Go to Rust

"We should rewrite it in Rust" is where a lot of good ideas go to die. A full rewrite is risky, slow, and usually unnecessary. The version that actually ships is far less dramatic: find the small slice of code responsible for most of your cost, replace only that with Rust, and leave the rest of your stack exactly where it is. Done right, this is a one- or two-week project with a measurable payoff, not a six-month bet.

Step 1: Measure before you touch anything

The cardinal rule of optimization applies double here: profile first. In almost every codebase, a tiny fraction of the code — a serialization loop, a parser, a pricing calculation, an image transform — burns the overwhelming majority of CPU. That hot path is your migration target. Everything else stays in Python or Go, where development speed matters more than raw throughput.

If you can't point at a flame graph and say "this function is 70% of our CPU," you're not ready to migrate. You're ready to profile.

Step 2: Carve a clean boundary

The hot path needs a narrow, well-defined interface: data in, data out, minimal shared state. The cleaner the boundary, the cheaper the migration and the easier it is to verify. If the hot code is tangled through global state, the first task is refactoring in your existing language to isolate it — before any Rust is written.

Step 3: Choose the integration seam

You don't call Rust from a void; you bind it into your current runtime. The right seam depends on your stack:

  • Python → Rust: PyO3 with maturin builds a native extension module you import like any other package. Your Python code barely changes — one function call now runs Rust underneath.
  • Go → Rust: expose a C ABI from Rust (#[no_mangle] extern "C") and call it over cgo, or split the hot path into a small service with a tight protocol.
  • Node / web → Rust: compile to WebAssembly and call it from JavaScript with near-native speed, in the browser or on the edge.
  • Any language: a small, fast Rust microservice behind gRPC is the most decoupled option when an in-process binding is awkward.
# After: same Python call site, Rust doing the work underneath.
from fast_core import price_book   # a PyO3 module
result = price_book(snapshot)     # 30x faster, identical signature

Step 4: Verify with a parity harness

Before the Rust version goes live, run it alongside the original on real inputs and assert the outputs match. This "shadow" approach catches the subtle differences — floating-point rounding, edge-case handling, encoding — that unit tests miss. Only when parity holds across production traffic do you flip the switch. It's the same discipline we use when reproducing on-chain swap math: trust the comparison, not the intention.

Step 5: Measure the win — honestly

Re-run the same profiler and benchmark you started with. Typical results for a genuinely hot path are large: order-of-magnitude latency reductions, big drops in CPU and memory, and the disappearance of GC-pause tail spikes. But report the number you actually measured, on your actual workload — not a headline from someone else's benchmark.

Why this beats a rewrite: you keep your team's velocity in the language they know for 95% of the code, you de-risk the change to a small, verifiable slice, and you capture most of the performance and cost benefit. Incremental adoption is how Rust actually enters a serious codebase.

When a bigger migration is justified

Sometimes the hot path is the product — a trading engine, a packet processor, a database internal — and the right answer is to build the core in Rust from the start. The decision rule is the same as choosing Rust for any new system: is latency a business metric, is correctness expensive to get wrong, does it run hot 24/7? If yes, see why Rust for low-latency systems. If the hot path is just a stubborn 5%, bind it and move on.

The takeaway

Don't rewrite — profile, isolate the hot path, bind Rust through the right seam (PyO3, FFI, WASM, or a small service), verify with a parity harness on real traffic, and measure the result honestly. That's how you get most of Rust's performance with a fraction of a rewrite's risk.

Have a hot path that's costing you? We do measured, incremental migrations to Rust — with a parity harness, not a leap of faith.