Introduction
Rewriting a large production system in another programming language is one of the most difficult forms of software maintenance.
The challenge is not converting syntax. A successful rewrite has to preserve existing behavior while improving performance, resource usage, reliability, maintainability, or another measurable engineering characteristic.
GitHub's work to rewrite a substantial portion of the Copilot runtime in Rust is a useful case study for developers working on performance-sensitive infrastructure.
The reported scale, roughly 800,000 lines of code, makes the project especially interesting. It shows why large runtime migrations require architectural boundaries, incremental adoption, compatibility testing, observability, and careful production rollout.
The broader lesson applies beyond Rust and beyond AI tooling: a language rewrite should solve a clearly measured engineering problem.
What Is a Copilot Runtime?
GitHub Copilot operates inside developer workflows, so its runtime needs to coordinate multiple activities between the editor and remote AI services.
A simplified architecture looks like this:
Developer
|
v
IDE / Editor
|
v
Copilot Runtime
|
+---- Context Collection
+---- Request Management
+---- Authentication
+---- Networking
+---- Caching
+---- Response Processing
|
v
AI Services
The runtime is therefore part of a latency-sensitive path.
If it spends too much time starting, processing context, allocating memory, or managing network operations, the delay can be visible to developers.
Why Rewrite a Mature Runtime?
Rewriting working software is expensive.
A team should have a measurable reason before replacing a mature implementation.
Typical motivations include:
Lower memory usage
Better concurrency
Reduced startup time
Improved latency
Greater runtime reliability
Better resource efficiency
Stronger memory-safety guarantees
Easier control over low-level behavior
Cross-platform requirements
The wrong starting question is:
Which language is better?
A better question is:
Which limitations of the current implementation are preventing the runtime from meeting its requirements?
That distinction matters because a language change is not automatically an architecture improvement.
Why Rust Is Interesting for Runtime Infrastructure
Rust combines native performance with compile-time memory-safety guarantees and explicit ownership.
That combination can be useful for systems that need:
High concurrency
+
Low latency
+
Controlled memory usage
+
Memory safety
Rust does not guarantee that an application will be faster.
A poorly designed Rust application can still have inefficient algorithms, excessive allocation, unnecessary copying, or slow I/O.
The benefit comes from combining the language's properties with appropriate architecture and measurement.
Rewriting 800,000 Lines Requires Incremental Migration
A rewrite of this scale should not normally be approached as:
Old System
|
| Rewrite everything
v
New System
A safer architecture is:
Existing Runtime
|
v
Identify Boundaries
|
v
Rewrite Component
|
v
Compatibility Layer
|
v
Production Validation
|
v
Migrate Next Component
This allows the old and new implementations to coexist during the transition.
It also makes rollback more practical.
Start With Architecture Boundaries
Before converting code, map the existing system.
A runtime might contain components such as:
Copilot Runtime
|
+---- Configuration
|
+---- Authentication
|
+---- Context
|
+---- Networking
|
+---- Caching
|
+---- Request Pipeline
|
+---- Response Handling
These boundaries provide potential migration units.
Strong interfaces reduce the amount of code that must change simultaneously.
A rewrite becomes much harder when every component depends directly on implementation details from every other component.
Preserve Existing Behavior First
A rewrite should initially preserve behavior wherever possible.
Suppose the original runtime performs:
Request
|
v
Collect Context
|
v
Prepare Request
|
v
Send Request
|
v
Process Response
The first Rust implementation should ideally perform the same logical operations.
Avoid changing everything simultaneously.
For example, changing the language, caching strategy, network protocol, authentication flow, and error model in one migration makes regressions difficult to isolate.
Behavioral compatibility provides a stable reference point.
Define a Compatibility Contract
Before migrating a component, define what its interface guarantees.
A contract can describe:
Inputs
Outputs
Error behavior
Timeout behavior
Retry behavior
Authentication
Serialization
Logging
Telemetry
Conceptually:
Input
|
v
Runtime API
|
+---- Success -> Expected Output
|
+---- Timeout -> Expected Timeout
|
+---- Failure -> Expected Error
The new implementation can then be tested against those expectations.
Differential Testing
One of the strongest techniques for a large rewrite is differential testing.
The same input is passed through both implementations:
Request
|
+--------+--------+
| |
v v
Old Runtime Rust Runtime
| |
v v
Result A Result B
| |
+--------+--------+
|
v
Compare
The comparison can identify:
Different outputs
Serialization changes
Error differences
Timeout differences
Edge-case regressions
Unexpected side effects
This is particularly valuable when the original system has years of accumulated behavior.
Rust Changes How Developers Think About Memory
Developers coming from garbage-collected languages need to adapt to Rust's ownership model.
Consider:
struct RequestContext {
repository: String,
file_path: String,
content: String,
}
A function can borrow the structure:
fn build_context(context: &RequestContext) -> String {
format!(
"{}:{}",
context.repository,
context.file_path
)
}
The function does not take ownership of the object.
This becomes important when processing large source files, repository information, editor state, and network payloads.
Developers have to reason about:
Ownership
Borrowing
Lifetimes
Allocation
Cloning
Shared state
Concurrency
That additional explicitness can be useful in performance-sensitive systems.
Avoid Unnecessary Cloning
Consider a large source file:
let copied_content = context.content.clone();
A clone may be completely appropriate in some situations.
But repeatedly copying large buffers can increase memory consumption and CPU work.
When ownership is not required, a borrowed value may be sufficient:
fn analyze(content: &str) {
// Read without taking ownership.
}
The important lesson is not "never clone."
It is to understand when a copy is necessary and measure its cost.
Concurrency in an AI Runtime
An AI coding assistant can perform multiple operations during a single request.
For example:
Request
|
+---- Read Editor Context
|
+---- Inspect Repository
|
+---- Read Configuration
|
+---- Prepare Network Request
|
+---- Update Cache
Some of these operations can potentially run concurrently.
A simplified asynchronous Rust operation might look like:
async fn collect_context() -> Result<Context, Error> {
let files = load_files().await?;
let repository = load_repository_info().await?;
Ok(Context {
files,
repository,
})
}
A production implementation may use concurrent tasks where operations are independent.
The important part is to control concurrency rather than creating unlimited asynchronous work.
Cancellation Is Part of Runtime Design
Developer actions can invalidate an AI request.
For example:
User edits code
|
v
AI request starts
|
v
User changes the file
|
v
Previous request is no longer useful
Continuing the old operation can waste CPU, memory, network capacity, and model resources.
A runtime should propagate cancellation through the entire request pipeline.
The architecture should allow:
Cancel Request
|
v
Runtime
|
+---- Stop Context Work
+---- Cancel Network Request
+---- Stop Processing
+---- Release Resources
Stopping only the outer request is not enough if internal operations continue running.
Memory Efficiency
A Copilot-style runtime can process large amounts of temporary data:
Source code
Editor buffers
Repository metadata
JSON messages
Model responses
Network buffers
Cached context
Memory behavior should therefore be measured directly.
Useful metrics include:
Metric | Why It Matters |
|---|---|
Resident memory | Overall runtime footprint |
Peak memory | Worst-case resource consumption |
Allocation rate | Indicates allocation pressure |
Request memory | Cost of individual operations |
Cache size | Persistent memory growth |
Startup memory | Cost of initialization |
A language rewrite should be judged using real workload measurements rather than assumptions.
Startup Performance
Developer tooling often starts alongside an IDE or editor.
That makes startup time important.
A runtime may be initialized when a developer:
Opens an IDE
Opens a repository
Switches projects
Restarts an extension
Recovers from an error
A useful measurement is:
Process Start
|
v
Initialization
|
v
Ready for Request
Optimizing only steady-state performance is not enough if startup remains slow.
Network Performance Still Matters
An AI runtime does not operate entirely locally.
A request may involve:
Context Collection
+
Serialization
+
Network Latency
+
Remote Processing
+
Response Processing
Total latency can therefore be represented as:
Total Latency =
Local Processing
+
Network Latency
+
Remote Processing
+
Response Handling
Improving local runtime performance cannot eliminate network latency.
A good migration measures each stage independently.
Cross-Platform Requirements
Developer tooling often needs to operate on multiple platforms.
Typical environments include:
Windows
macOS
Linux
The runtime may need platform-specific handling for:
File systems
Paths
File watchers
Processes
Environment variables
Certificates
Permissions
Native integrations
Compilation across platforms is not enough.
The runtime needs behavioral testing on each supported operating system.
Observability During Migration
A large rewrite requires better observability than a normal feature release.
If the old and new implementations coexist, compare them using common metrics.
For example:
Runtime Comparison
|
+---- Old
| |
| +---- Latency
| +---- Errors
| +---- Memory
|
+---- Rust
|
+---- Latency
+---- Errors
+---- Memory
Useful telemetry includes:
Request latency
Time to first response
Error rate
Crash rate
Memory consumption
CPU utilization
Cancellation rate
Network failures
Tool execution failures
These measurements provide evidence for migration decisions.
Gradual Production Rollout
A large runtime should not normally be switched for every user at once.
A staged rollout can look like:
Internal Users
|
v
Small Production Group
|
v
Larger Group
|
v
Broad Deployment
At each stage, compare the new runtime with established baseline measurements.
If a regression appears, the rollout can be paused before it affects the entire user population.
Feature Flags and Rollback
A runtime migration benefits from a reliable mechanism for selecting the implementation.
Conceptually:
Request
|
v
Runtime Selection
|
+---- Old Runtime
|
+---- Rust Runtime
This provides an escape path if the new implementation produces an unexpected production issue.
The exact mechanism can be a feature flag, deployment configuration, routing layer, or another controlled selection system.
The important part is that rollback should be designed before the migration begins.
What the Rewrite Teaches About Language Selection
The biggest lesson is not that every application should be rewritten in Rust.
Different applications have different constraints.
For runtime infrastructure, the team may prioritize:
Memory safety
Concurrency
Latency
Resource efficiency
Native execution
Predictable behavior
Another system might prioritize:
Rapid development
Library availability
Developer familiarity
Business feature velocity
Language selection should therefore follow workload requirements.
Common Mistakes
Rewriting Everything at Once
A complete cutover creates a very large failure surface.
Combining Too Many Changes
Changing the language, architecture, protocol, caching model, and behavior at the same time makes debugging difficult.
Measuring Only CPU Time
Memory, latency, startup, network performance, and reliability can be equally important.
Ignoring Real Workloads
Synthetic benchmarks may not represent how developers actually use the product.
Removing the Old Implementation Too Early
The original implementation can be an important behavioral reference.
Treating Compilation as Validation
A program that compiles can still behave differently from the system it replaces.
Ignoring Operational Tooling
The rewrite also affects debugging, monitoring, packaging, deployment, and support processes.
Best Practices for Large Runtime Rewrites
Identify the measurable problems before choosing the language.
Define success criteria.
Map the existing architecture.
Establish clear component boundaries.
Preserve behavior during the initial migration.
Create compatibility contracts.
Use differential testing.
Measure real production-like workloads.
Track memory and CPU independently.
Design cancellation into asynchronous operations.
Test every supported operating system.
Maintain strong observability.
Roll out gradually.
Keep rollback available.
Remove legacy components only after sufficient validation.
Advantages of Rust for Runtime Components
Rust can be a strong fit for certain runtime workloads because it provides:
Compile-time memory-safety guarantees
Explicit ownership
Native execution
Strong concurrency primitives
No traditional garbage collector
Fine-grained resource control
Mature support for asynchronous programming
These characteristics can be valuable when the runtime is resource-sensitive.
Tradeoffs of a Rust Rewrite
A rewrite also introduces costs.
Developers must become comfortable with concepts such as:
Ownership
Borrowing
Lifetimes
Traits
Async programming
Rust tooling
Native build systems
The organization also needs to support the new technology over the long term.
A rewrite therefore needs more than an engineering prototype. It needs a sustainable maintenance model.
When Does a Large Rewrite Make Sense?
A rewrite becomes easier to justify when there is a clear combination of measurable problems and a practical migration strategy.
For example:
Existing Limitations
|
+---- Performance
+---- Memory
+---- Reliability
+---- Concurrency
|
v
Measurable Goals
|
v
Incremental Migration
|
v
Production Validation
If the existing system already meets its requirements, a rewrite may introduce substantial risk without enough measurable benefit.
Practical Migration Checklist
Before starting a large runtime rewrite:
Document the current architecture.
Identify measurable performance or reliability problems.
Define target metrics.
Identify independent components.
Establish compatibility requirements.
Build regression tests.
Select a small migration candidate.
Implement the replacement.
Compare old and new behavior.
Measure resource usage.
Test across supported platforms.
Deploy gradually.
Monitor production telemetry.
Keep a rollback mechanism.
Remove legacy components only after validation.
Summary
GitHub's work on moving a large portion of the Copilot runtime to Rust demonstrates the complexity of modern runtime engineering.
The interesting lesson is not simply that hundreds of thousands of lines were rewritten. The deeper lesson is how a large production system can be migrated without treating the rewrite as one enormous replacement project.
Architecture boundaries, compatibility contracts, differential testing, resource measurement, cancellation, observability, and staged rollout all become essential.
Rust can provide valuable properties for performance-sensitive runtime components, but the language alone does not guarantee better software.
A successful rewrite starts with measurable engineering problems, uses the new technology to address those problems, and validates every major step against real workloads.
For teams considering a similar migration, the goal should not be to rewrite code for its own sake. The goal should be to build a runtime that is measurably better suited to the workload it has to serve.

Join the conversation! Your thoughts help the community grow.