THE MIGRATION LINE
INCIDENT // VERCEL-2023 IN PROGRESS

Having decided to move Turborepo from Go to Rust, Vercel's team rejected a ground-up rewrite. Instead they ran both languages at once, porting piece by piece behind a Rust entry point they call the Rust-Go-Rust sandwich -- and hit a segfaulting Alpine Linux binary along the way.

VERCEL · 2023 · LANGUAGE / RUST / GO / FFI
System stress over time Breach at T+3
Incremental port, not a rewrite Approach
v1.7.0 First hybrid release
5,742 hours (product eng + CI) Time saved, one week
7-year-old GitHub issue Alpine bug's age
THE SITUATION

Rust was decided. Now someone has to move 70,000 lines of Go

Vercel’s team had already decided to move Turborepo from Go to Rust. The open question was how. Turborepo was a working, widely-installed CLI, not a greenfield project — so whatever the porting strategy was, it couldn’t ask users or the team to simply stop while the migration happened.

THE COMPLICATION

Why a full rewrite was rejected

The team laid out three reasons a ground-up rewrite doesn’t work for software like this. Halting all feature work to rewrite means chasing a moving target as the old codebase keeps growing underneath you. A rewrite doesn’t guarantee a better experience for users and often introduces breaking changes along the way. And code left behind during a rewrite doesn’t just sit there harmlessly — unused code becomes, in their words, “a breeding ground for bugs.”

THE QUESTION

How do you run two languages doing the same job at once, without the seams showing?

So the constraint was: port incrementally, keep shipping features the whole time, and make sure Go and Rust code can call each other cleanly enough that users never notice the seam. That meant answering a narrower, harder question — what’s the actual mechanism that lets a Rust binary hand off to Go code, and back, across every platform Turborepo ships on?

THE ANSWER

The Rust-Go-Rust sandwich, and the Alpine binary that wouldn't behave

The team built what they call the Rust-Go-Rust sandwich: Rust sits at the entry point and decides, command by command, whether to run its own Rust implementation or hand off to the existing Go code. The first feature ported this way was global turbo, a machine-wide install command — the “Rust shim” wrapping the existing Go implementation, compiled via CGO into a C static library. The CLI argument parser came next, rebuilt in Rust with the clap crate. Rather than build complex cross-platform C types to pass arguments like --cwd between the two languages, they serialized arguments to JSON strings, leaning on serde in Rust and the JSON handling Go already had.

That approach survived Windows — where Go’s exclusive use of the MinGW toolchain clashed with Rust’s initial MSVC build until Rust switched to MinGW too, and where Universal Naming Convention paths needed the dunce crate to canonicalize cleanly. Alpine Linux was worse. Alpine has no glibc, so the team tried compiling statically against musl instead — and the Go runtime “would return a segmentation fault” with “a corrupted stack.” The cause turned out to be a seven-year-old GitHub issue: Go cannot be compiled as a C static library with musl at all. The fix was architectural — stop linking Go and Rust into one binary, and instead compile them as two separate binaries, with the Rust executable calling the Go binary over the CLI and passing the same JSON-serialized arguments they’d already built. Version 1.7.0 shipped as the first hybrid Go-Rust release using that strategy.

THE TAKEAWAY

What incremental porting actually costs

The team was candid about the price of this approach: cross-platform, cross-language release engineering is “extremely challenging,” and JSON serialization added performance overhead they accepted because the argument payloads were only hundreds of bytes. Incremental porting also demands heavy automated and manual testing up front, to pin down exact edge cases before a single line moves. Despite all of it, they maintain the incremental port “has proven to be the correct choice for us strategically.” By the week this post published, Turborepo had saved 5,742 hours of product-engineering and CI machine time at Vercel in that week alone.

Porting a live system in place is slower and messier than a clean rewrite — but it never stops shipping, and that’s the whole trade.

Source — read the original

https://vercel.com/blog/how-turborepo-is-porting-from-go-to-rust

A plain-language, AI-drafted and human-edited retelling of the article published on vercel.com, reorganized and explained in our own structure and words, with original analysis in the editor's note above. The facts, numbers, and decisions belong to the original author and are not altered. For the full depth, read the source.

← All systems