Vercel CEO: Turborepo Go→Rust Migration ROI 'Controversial'; AI Agents Change the Calculus

Vercel incrementally migrated Turborepo from Go to Rust, overcoming cross-platform hurdles like Windows UNC paths and Alpine musl incompatibilities via JSON serialization and separate binaries, but CEO Guillermo Rauch now calls the ROI 'controversial' as AI agents lower migration costs and shift language selection priorities.

TonyBai
TonyBai
TonyBai
Vercel CEO: Turborepo Go→Rust Migration ROI 'Controversial'; AI Agents Change the Calculus

Background: Vercel CEO's Tweet Sparks Debate

In October 2026, Vercel CEO Guillermo Rauch posted a long tweet (nearly 400k views) revealing that Turborepo's migration from Go to Rust was complete but the ROI was "quite controversial" internally. He agreed with DHH that Rust is better for low-level OS access, but noted the human migration cost was very high. The key shift: "what's best for humans" no longer equals "what's best for business" because AI agents are changing who writes code.

Timeline of the Migration

March 2023: Vercel blog "Why Turborepo is migrating from Go to Rust" announces incremental migration starting v1.7.

July 2023: Blog "How Turborepo is porting from Go to Rust" details strategy and cross-platform challenges.

v1.7.0: First mixed Go/Rust release.

October 2026: Rauch's tweet retrospective.

Why Leave Go? Design Priority Mismatch

Go's Strengths Target Data-Center Services

Go excels at network computing in data centers: goroutine-per-request, Context API, standard library server infrastructure. It prefers simplicity over expressiveness, pushing more errors to runtime. For a data-center service, rollbacks are cheap.

Turborepo Runs on User Machines

Turborepo is software installed on users' machines. Every error has higher cost. Rust's community prioritizes "correctness over API abstraction", which matches Turbo's needs for process management, filesystem, low-level OS concepts, and software distribution.

Concrete Example: File Permissions

Go allows setting Unix-style permission codes (e.g., rw-r--r--), but this abstraction doesn't work on Windows, which lacks precise file permissions. Go silently accepts the call on Windows even though it has no effect. Rust forces explicit platform declaration: code that sets Unix permissions must be guarded with #[cfg(unix)] and will fail to compile on Windows.

// Illustrative code: only compiles on Unix, excluded on other platforms
#[cfg(unix)]
fn make_executable(path: &std::path::Path) -> std::io::Result<()> {
    use std::os::unix::fs::PermissionsExt;
    std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o755))
}

This exposes complexity early, ensuring the team knows exactly what the code does before shipping to users.

CGO's Global Cost and Turbopack Duplication

Two practical factors drove the decision:

CGO's global cost: Turborepo increasingly depended on C libraries like zstd for cache compression. Using CGO forces the entire codebase to build with the slower C toolchain. Rust's bindgen, cxx, and crates like git2 (wrapping libgit2) provide safe bindings without global build changes.

Turbopack team already used Rust: Both teams shared a codebase and solved similar problems twice (once in Go, once in Rust). Unifying on Rust enabled shared tooling, e.g., adopting Turbopack's file-watching for cross-workspace hot reloads.

A softer reason: the team wanted to write Rust; Stack Overflow surveys show high Rust satisfaction and it's the second most desired language after JavaScript for web developers.

Incremental Porting Strategy: No Big Rewrite

Why Not a Full Rewrite?

Vercel evaluated a full rewrite but rejected it for three reasons:

Would require pausing new feature development to chase a growing legacy codebase.

No guarantee of better UX; matching every edge case of the old version is extremely hard.

Produces large amounts of unexercised new code, which becomes a bug reservoir even with tests.

Incremental Porting Approach

They chose incremental porting: move code piece by piece, letting old and new run simultaneously. Each ported piece must behave identically — pure translation, no improvements. This allows using the same tests to verify parity and keeps feature delivery ongoing. The trade-off: significant extra complexity for Go-Rust interop, slowing initial velocity.

First Step: global turbo as a Rust Shim

They started by implementing a brand-new, small feature in Rust: global turbo, which lets users install turbo as a global command that prefers the local repo version and falls back to the global binary. This validated the Rust toolchain integration while touching minimal Go code.

The implementation used a thin Rust shim wrapping the existing Go code. The Go portion was compiled via CGO into a C static library and linked into the Rust binary. Because global turbo only needed config reading and filesystem traversal, the integration was quick.

This architecture was later dubbed the "Rust-Go-Rust sandwich" : Rust entry point decides whether a command runs in Rust or Go; Go can also call back into Rust, providing a permanent path toward full Rust.

Rust-Go-Rust sandwich architecture diagram
Rust-Go-Rust sandwich architecture diagram

CLI Parsing and JSON-Based FFI

While building global turbo, they moved CLI argument parsing (using the clap crate) to Rust. The challenge: passing parsed arguments from Rust to Go. Instead of complex C FFI types, they chose a simple approach: serialize arguments to JSON in Rust (using serde), pass the JSON string to Go, which deserializes into its existing structs. The payload is only a few hundred bytes, so JSON overhead is negligible.

use serde::Serialize;

#[derive(Serialize)]
struct Args {
    cwd: Option<String>,
    command: String,
    // ... other fields
}

// Serialize to JSON string for Go side
let payload = serde_json::to_string(&args)?;

This "humble" design later proved crucial when deeper FFI issues emerged.

JSON-based FFI between Rust and Go
JSON-based FFI between Rust and Go

Cross-Platform Pitfalls: The Real Hard Part

Windows: MSVC vs MinGW and UNC Paths

Toolchain mismatch: Go uses MinGW on Windows; Rust defaulted to MSVC, causing runtime issues. Solution: switch Rust to MinGW.

UNC paths: Windows path canonicalization returns UNC (Universal Naming Convention) paths, which are not universally supported — sometimes even Windows rejects them, yielding "invalid path" errors. Fixed by using the dunce crate to normalize paths to non-UNC form.

Alpine Linux: glibc, musl, and a 7-Year-Old Go Issue

Vercel uses Alpine for lightweight containers. Alpine lacks glibc; compatibility layers ( gcompat, libc6-compat) failed because Rust required a newer glibc than supported. They attempted fully static binaries using musl (glibc cannot be statically linked due to licensing). Rust can target musl easily; Go doesn't use libc by default.

However, the mixed binary crashed with a segmentation fault — stack corruption originating from the Go runtime. Investigation uncovered a 7-year-old GitHub issue (golang/go#13492): Go cannot be compiled as a C static library together with musl. This threatened to block Alpine support entirely.

Final Solution: Two Separate Binaries

The earlier JSON-based FFI design paid off. They split Go and Rust into two independent binaries. The Rust binary invokes the Go binary via command line, passing the serialized JSON arguments. Because the FFI surface was already minimal (just a JSON string), the code change to switch from in-process linking to process invocation was tiny. The first mixed release was v1.7.0 .

Two-binary deployment architecture
Two-binary deployment architecture

Vercel's 2023 Retrospective: Four Lessons

Serialization is FFI's friend. Using JSON (mature in both languages) minimizes the FFI surface, avoiding a whole class of cross-platform, cross-language bugs. Cost: serialization overhead, only suitable for small payloads or non-critical paths.

Porting requires thorough preparation. Incremental porting works but demands rigorous testing and strategy. CLI parsing details, config loading order — every detail must match. Advice: write tests before porting ; they become the specification.

Cross-compatibility is hard. Multi-platform, multi-language release engineering is extremely challenging. Each platform, language, and compiler has quirks; more moving parts mean more failure modes.

For us, porting was worth it. Despite brutal debugging, they kept delivering features and fixes during migration. The 2023 conclusion: worth it .

2026 Reassessment: The Calculus Has Changed

Rauch's Core Argument

Comparing the 2023 blog with the 2026 tweet reveals a perspective shift.

2023 accounting:

Benefits: compile-time correctness, low-level OS capabilities, shared ecosystem with Turbopack, happier team.

Costs: interop complexity, cross-platform release challenges, slower dev velocity.

Verdict: Humans accept short-term pain for long-term gain.

2026 accounting:

Rust is technically superior.

But human migration cost is "substantive".

Go is fast, elegant, easy to iterate — "even knowing Rust is better", the team hesitated.

Today, "best for humans" ≠ "best for business".

The core constraint in language choice used to be human programmer experience and efficiency. As AI agents become primary code authors, that constraint loosens.

Why Agents Change the Migration Math

Migration cost structure shifts. The most labor-intensive 2023 work — module-by-module translation, behavior verification, test writing — is exactly the repetitive work agents excel at. High human migration cost means delaying migration may become a hidden cost in the agent era.

Language ergonomics weight declines. Go's iteration speed and elegant design benefit humans. Rust's compile-time constraints and type system are learning curves for humans but act as natural guardrails for agents , because the compiler catches entire error classes automatically.

Future language evaluation criteria may change. Rauch's closing remark implies Rust was designed for human authors; when agents dominate, the next generation of languages/toolchains will be designed under new constraints. This is a judgment, not a certainty, but teams should watch.

Three Caveats Against Overinterpretation

This doesn't mean Go is bad. Rauch calls Go "very fast, beautifully designed, easy to iterate". Go's dominance in network services and cloud infrastructure remains. Turborepo's migration was driven by its special scenario : a build system running on user machines needing fine-grained OS interaction and compile-time guarantees.

"Lower migration cost" ≠ "zero migration cost". The hardest 2023 challenges — cross-platform release engineering (MinGW, UNC paths, musl vs Go runtime) — cannot be solved by code translation alone; they require real-environment debugging and trade-offs. Even powerful agents need reliable verification systems , making "write tests first, then port" more important, not less.

"Greener pastures" is a prediction, not a roadmap. Rauch is "fairly certain" better options will emerge, but hasn't specified what. Until new toolchains materialize, using the best-fit language today remains the soundest strategy.

Summary

This is a fascinating "dual timeline" story. In 2023, Vercel engineers used JSON and two binaries to work around a 7-year-old Go-musl issue, successfully shipping Turborepo on Rust. In 2026, the CEO looks back and says: the migration is done, but if we evaluated it today, the math might differ.

This isn't a repudiation of the past decision, but a reminder: we are in a moment where the fundamental assumption of "who writes code" is being rewritten. When that changes, many "obvious" engineering trade-offs deserve re-evaluation.

Is Rust the endpoint? Perhaps Rauch is right: greener pastures lie ahead. 🦀

References

Guillermo Rauch (@rauchg) tweet: https://x.com/rauchg/status/2106863842450133114 Vercel blog: Why Turborepo is migrating from Go to Rust (Mar 7, 2023): https://vercel.com/blog/turborepo-migration-go-rust Vercel blog: How Turborepo is porting from Go to Rust (Jul 21, 2023):

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

Go issue (Go cannot compile with musl as C static library):

https://github.com/golang/go/issues/13492
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

migrationcross-platformAI agentsFFIRustGoTurborepoincremental porting
TonyBai
Written by

TonyBai

Tony Bai's tech world (tonybai.com). Not satisfied with just "knowing how", we strive for mastery. Focused on Go language internals, high-quality engineering practices, and cloud‑native architecture, exploring cutting‑edge intersections of Go and AI. Gophers who pursue technology are welcome—follow me and evolve with Go.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.