The Python service processed webhook events from a payment provider. It received JSON payloads, validated them, transformed the data, and wrote results to a database. Nothing exotic.

It ran on 8 workers to keep up with load. It occasionally OOM-killed workers under burst traffic. The P99 latency was 340ms for operations that should have taken 20ms. A profiler showed 60% of CPU time in JSON parsing and string manipulation.

The Rust replacement runs on a single thread. P99 is 18ms. Memory usage is 12MB instead of 800MB across 8 workers.

Here is what the migration actually looked like.

The Python Service: What Was in 3,000 Lines

The Python service was not 3,000 lines of pure logic. It was:

  • ~400 lines of actual business logic (validation rules, transformation functions)
  • ~600 lines of Django REST Framework boilerplate (serializers, viewsets, URL routing)
  • ~800 lines of Celery task definitions and configuration
  • ~500 lines of tests
  • ~700 lines of utilities (logging, config loading, helper functions)

A lot of that code existed because of the framework’s conventions, not because the problem required it. The validation logic was the actual service.

What the Rust Replacement Does

The Rust binary:

  • Listens on a port for incoming HTTP POST requests
  • Parses the JSON body using serde_json
  • Validates the payload structure using custom validation logic
  • Transforms the data
  • Writes to Postgres via sqlx
  • Returns a 200 response
use axum::{routing::post, Router, Json, http::StatusCode};
use sqlx::PgPool;
use serde::{Deserialize, Serialize};

#[derive(Deserialize)]
struct WebhookEvent {
    event_type: String,
    object_id: String,
    amount_cents: i64,
    currency: String,
    metadata: serde_json::Value,
}

async fn handle_webhook(
    pool: axum::extract::State<PgPool>,
    Json(event): Json<WebhookEvent>,
) -> Result<StatusCode, AppError> {
    validate_event(&event)?;
    let transformed = transform_event(&event);
    save_event(&pool, &transformed).await?;
    Ok(StatusCode::OK)
}

#[tokio::main]
async fn main() {
    let pool = PgPool::connect(&std::env::var("DATABASE_URL").unwrap()).await.unwrap();
    let app = Router::new()
        .route("/webhook", post(handle_webhook))
        .with_state(pool);
    axum::serve(tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(), app)
        .await.unwrap();
}

The actual implementation is 300 lines including validation, transformation, error handling, and tests. No framework, no ORM, no task queue.

Where the 10x Code Reduction Came From

The Python service used Django REST Framework and Celery. Both are excellent tools that solve real problems - but they also bring conventions that create boilerplate.

A DRF serializer for a 5-field model is 30 lines. In Rust with serde, it is a derive macro on a struct: 6 lines.

A Celery task that processes an event and saves to a database is 40 lines of definition, another 30 lines of configuration. In Rust, it is just a function call.

The validation logic in Python was 80 lines of conditional checks. In Rust with early returns and the ? operator, it is 40 lines that are arguably more readable.

The Performance Difference: Why It Is Not Surprising

Python’s overhead for this workload:

  • Django middleware stack processes every request through 12 middleware layers
  • JSON parsing in Python processes bytes through an interpreter
  • Django ORM generates SQL, serializes parameters, parses results through Python objects
  • 8 workers needed because Python is single-threaded per process

Rust’s overhead for this workload:

  • Axum middleware is zero-cost abstractions compiled away
  • serde_json parses bytes directly to Rust structs via generated code
  • sqlx compiles SQL at build time and uses async I/O
  • Tokio handles concurrent requests without multiple processes

The Python service was not “doing Python badly.” It was doing exactly what the framework intended. The framework’s generality had a cost that mattered at this service’s scale.

What the Migration Actually Cost

Honest accounting:

Time: Three days of implementation, two days of testing and validation.

Difficulty: The Rust borrow checker required real learning. Getting the async context right with axum took a day of reading. The sqlx macro for compile-time SQL verification caught a subtle type mismatch that would have been a runtime bug.

Operational change: The Python service had a Dockerfile. The Rust service has a different Dockerfile. Deployment is simpler because there is no Python runtime, no virtual environment, and the binary is statically linked.

Monitoring: Python had Sentry for error tracking and Django’s built-in request logging. The Rust service uses tracing crate with JSON output, which works with the same log aggregation pipeline.

When This Trade-Off Makes Sense

This migration was worthwhile because:

  1. The service had a well-understood, stable interface (a single HTTP endpoint)
  2. The performance issues were real and costly (8 workers vs 1)
  3. The logic was relatively simple (validate, transform, save)
  4. The team had at least one engineer comfortable with Rust

It would not have been worthwhile if:

  • The service had complex business logic that was still changing
  • The team had no Rust experience (the learning curve time cost would have been much higher)
  • The Python service was fast enough with tuning

The Cases Where Rust Excels

Beyond this specific example, Rust consistently wins over Python for:

  • CLI tools that run frequently - startup time and binary distribution
  • Data processing pipelines - throughput-bound workloads
  • Network proxies and middleware - latency-sensitive path
  • Anything with tight memory requirements - embedded, edge, serverless with cold starts

Rust loses to Python for:

  • Prototyping and exploration - Python iteration speed is higher
  • Data analysis - pandas/numpy/scipy ecosystem is irreplaceable
  • ML training - PyTorch is Python-first
  • Anything where the bottleneck is not CPU or memory

Bottom Line

The Python-to-Rust rewrite narrative is often oversold. Most Python services are not CPU-bound and would not benefit from a Rust rewrite. But for services where the bottleneck is legitimately compute - JSON parsing, string processing, data transformation - Rust’s performance advantage is not incremental. It is categorical.

The code size reduction is a real secondary benefit. Fewer lines that do the same thing means less to maintain, test, and debug. The migration cost was five days for a service that runs 24/7 at meaningful scale. That math works.