JOURNAL

GitHub Rewrote the Copilot Runtime in Rust. The Interesting Part Is How Slowly They Did It.

GitHub ported its Copilot language-server runtime from Node.js to Rust over roughly 128 pull requests. The incremental path, not the language choice, is the part worth studying.

Diagram comparing the old Node.js subprocess runtime (JSON-RPC over pipes, 5.25 second startup) to the new in-process Rust runtime called over a C ABI (292 millisecond startup), with the out-of-process JSON-RPC server kept as a fallback path.

Read with AI

Choose content to copy and paste into your AI assistant. Nothing is sent automatically. CMS content is converted to Markdown; original Markdown is used when available.

GitHub spent the back half of 2026 replacing the runtime underneath its Copilot language server, moving the core from Node.js to Rust. That’s not news by itself; language-server runtimes get rewritten for performance reasons all the time. What’s worth your attention is the shape of the migration, not the destination language. GitHub didn’t stop the world and ship a rewrite. It ran roughly 128 pull requests over about fourteen and a half weeks, kept the old Node.js path alive as a fallback the entire time, and only switched the default once the new path had earned it. That’s a specific, teachable decision, and it’s the opposite of how most high-profile language rewrites get done.

The technical case for the rewrite is straightforward. The old architecture ran the Copilot language server as a separate Node.js process per editor client, talking to it over JSON-RPC across stdio pipes. Every editor window meant another subprocess, another JSON-RPC handshake, another chunk of startup latency. GitHub’s own reporting on the project put Node.js cold-start time at roughly 5.25 seconds versus about 292 milliseconds for the new in-process Rust runtime, called through a C ABI via FFI bindings rather than spawned as a separate process. That’s close to an 18x improvement on startup alone, and it compounds across every editor window a developer opens in a given day.

Diagram comparing the Copilot runtime before and after the Rust port: before, editor clients spoke JSON-RPC to a Node.js subprocess with a 5.25 second startup; after, the same clients call an in-process Rust runtime over a C ABI with a 292 millisecond startup, while the original JSON-RPC server stays available as a fallback path.
Copilot’s runtime before and after the Rust port. The JSON-RPC fallback path didn’t disappear when the new path shipped — it’s still there.

Why the slow path is the actual story

A rewrite of a runtime that sits in the hot path of every keystroke in every supported editor is a dangerous kind of change. Get it wrong and you don’t get a bug report, you get silent latency regressions or crashes across an enormous, heterogeneous install base running different editors, OS versions, and extension combinations GitHub can’t fully enumerate in a test matrix. The team’s response to that risk was architectural, not just procedural: build the new Rust core behind the same interface the old Node.js server exposed, ship it gated, and keep the JSON-RPC server as a standing fallback rather than a deprecated code path scheduled for deletion. The old server didn’t become legacy debt to clean up later — it stayed load-bearing until the new path had demonstrated it could carry the same weight.

Splitting the work into roughly 128 pull requests over fourteen and a half weeks is the part that takes discipline most teams don’t have when a rewrite gets greenlit. It’s slower to plan, slower to review, and it means carrying two runtimes in the codebase simultaneously for months. The payoff is that each of those 128 PRs is independently revertable, independently testable, and independently bisectable if something breaks in production. A single PR that silently regresses completion latency for one obscure editor configuration gets caught and rolled back without taking down the whole migration. A 50,000-line big-bang PR replacing the entire runtime at once doesn’t give you that option — when something breaks, you’re debugging the whole rewrite, not one PR’s worth of change.

The counter-example worth sitting with

It’s worth naming the opposite approach, because it also worked. Jarred Sumner’s rewrite of Bun’s JavaScript-to-native tooling from Zig to Rust in roughly eleven days is the highest-profile recent example of a big-bang agentic rewrite landing cleanly. Sumner has described using Claude Code to drive large portions of that port in a short, concentrated burst rather than a long incremental campaign. Both approaches produced working software. The difference is risk surface and team structure, not raw competence. Bun’s port touched tooling with a smaller, more contained blast radius and a team that could hold the entire rewrite’s state in a few people’s heads at once. GitHub’s port sits underneath an install base spanning VS Code, Visual Studio, JetBrains IDEs, Vim, and whatever else Copilot ships for, with a user population too large and varied to let any one person reason about every failure mode before merging.

That’s the actual decision criterion, and it has nothing to do with which language is objectively better or which team is more skilled. Big-bang rewrites make sense when the blast radius is small enough that a handful of engineers can hold the full scope in their heads, the rollback story is simple (revert the one PR, or just don’t merge it), and the cost of being wrong for a few hours is low. Incremental, fallback-protected rewrites make sense when the blast radius is an install base you can’t fully enumerate, the system sits in a latency-sensitive hot path, and being wrong even briefly means a visible regression for users who never opted into being part of your migration.

What this means if you’re planning one of these

If you’re staring at a migration of this shape — replacing a component that’s load-bearing for a user base you can’t fully observe — the GitHub pattern gives you a concrete checklist rather than a vibe. Keep the old path alive and callable, don’t schedule its deletion until the new path has run in production long enough to earn trust. Split the migration into units small enough that each one is independently revertable; if a single PR in your plan can’t be safely reverted on its own, it’s too big. Gate the new path behind a flag you control, so a regression is a config change, not an emergency rollback deploy. And be honest about your actual blast radius before picking a strategy — the honest answer for most internal tooling migrations is a blast radius small enough that the incremental approach is overkill, and for most user-facing infrastructure sitting in a hot path, it’s the only approach that doesn’t gamble with other people’s workday.

The Rust numbers are the part that’ll get cited in the comparison tables. The actual transferable lesson is upstream of the language: GitHub treated the migration itself as the risky artifact, not just the destination code, and built a process that let it fail in small pieces instead of all at once.

Discussion

Comments are reviewed before publication. Your email is kept private.

← Back to allĐọc tiếng Việt