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

  1. Identify the measurable problems before choosing the language.

  2. Define success criteria.

  3. Map the existing architecture.

  4. Establish clear component boundaries.

  5. Preserve behavior during the initial migration.

  6. Create compatibility contracts.

  7. Use differential testing.

  8. Measure real production-like workloads.

  9. Track memory and CPU independently.

  10. Design cancellation into asynchronous operations.

  11. Test every supported operating system.

  12. Maintain strong observability.

  13. Roll out gradually.

  14. Keep rollback available.

  15. 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:

  1. Document the current architecture.

  2. Identify measurable performance or reliability problems.

  3. Define target metrics.

  4. Identify independent components.

  5. Establish compatibility requirements.

  6. Build regression tests.

  7. Select a small migration candidate.

  8. Implement the replacement.

  9. Compare old and new behavior.

  10. Measure resource usage.

  11. Test across supported platforms.

  12. Deploy gradually.

  13. Monitor production telemetry.

  14. Keep a rollback mechanism.

  15. 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.