
Autopsying Anthropic's 40% CPU Win: Which Levers Actually Moved in the Claude-Assisted Rust Port
On this page
Introduction: What the 36Kr Report Claims About the Claude-Assisted Rust Port — and What It Doesn't
An Anthropic team with no prior Rust experience reportedly leaned on Claude to refactor a core API across languages, and CPU consumption dropped 40% while latency fell 20%. This post autopsies that claim: which levers plausibly moved those numbers, what the report omits, and how to benchmark, review, and roll back a Claude-assisted Rust port before you bet an architecture on it.
Two big numbers attached to a big process claim. But in the copy I can actually see, that's all it is — a headline and one sentence. The feed entry is dated 2026-10-29. No workload description, no hardware, no measurement window, and no first-party engineering write-up I could find to match it. So this is not a reproduction. It's a teardown: what would have to be true for those numbers to hold, which levers plausibly moved them, and what you should demand before a result like this changes your architecture.
My position up front: a 40% CPU cut from a cross-language port is almost never a language win. It's a request-path rewrite that the port forced you to do anyway. Rust gave the team permission to delete work. Claude made the typing cheap. Neither of those is the same as "Rust is faster," and conflating them will send you off porting codebases that never needed porting.
The headline numbers: 40% less CPU, 20% lower latency, zero prior Rust
The report's three assertions, taken at face value:
- The team had no prior Rust experience.
- Claude was used to assist a cross-language refactor of a core API.
- The outcome was ~40% lower CPU consumption and ~20% lower latency.
What it leaves out: which model, how much of the code the model actually authored, what the source language was, and whether those numbers came from production or a benchmark. Every one of those gaps matters more than the headline does.
Why this needs an autopsy rather than a victory lap
Because "AI wrote Rust and it got faster" is a causal story with at least four confounders stacked on top of each other:
- The rewrite itself. A port is a rewrite. Rewrites routinely produce double-digit wins in the same language, because the second implementation has a spec and a profiler.
- The concurrency model. Cross-language ports almost always change the threading model, and threading is where CPU efficiency actually lives or dies.
- The measurement boundary. "40% less CPU" is meaningless until you say per what — per request, per second, per node, per dollar.
- The workload. Wins from allocation and GC reduction scale with load. A number measured at low concurrency can collapse to noise at saturation, or triple.
An honest headline would read: "We rewrote the hot path, we deleted most of its allocations and copies, we stopped oversubscribing the scheduler, and CPU dropped 40%. Also, an LLM typed a lot of it." Less shareable. Considerably more useful.
Separating Confirmed Facts From Inference in the 36Kr Report
What the report actually states (team, task, tool, numbers)
| Claim | Source | Status |
|---|---|---|
| Cross-language refactor of a core API | 36Kr report | Reported, details unknown |
| Team had zero prior Rust experience | 36Kr report | Reported |
| Claude assisted the refactor | 36Kr report | Reported; model and extent unknown |
| ~40% lower CPU consumption | 36Kr report | Reported, unverified, denominator unspecified |
| ~20% lower latency | 36Kr report | Reported, unverified, percentile unspecified |
| A first-party Anthropic engineering post exists | My search | Not found — absence of evidence, not evidence of absence |
That last row deserves a plain statement. I could not locate a primary Anthropic write-up carrying these numbers. If one exists, it probably contains the workload and hardware detail that would settle most of the questions below. I'm not asserting it doesn't exist.
What is missing: workload shape, hardware, baseline language, request mix
These are the fields I'd want filled in before treating the result as transferable:
- Source runtime. JVM? Node? Python? Go? The answer changes the mechanism. If the baseline was a GC language, a large share of the win is GC pressure, not instruction count. If the baseline was Go, the win is more likely scheduler and allocation related — and 40% is harder to explain.
- Workload shape. Requests per second, payload sizes, endpoint mix, read/write ratio, cache hit rate, TLS termination point.
- Hardware. Instance type, vCPU count, co-tenancy, whether the client was on the same host.
- Measurement window. Wall clock? Warm? Steady state? Under fault injection?
- Percentile. "20% lower latency" at p50 versus p99 are different achievements. At p99 it implies queueing was fixed. At p50 it might just be a faster JSON path.
- Scope of the change. Did the port also change deployment topology, the load balancer, batch sizes, or connection reuse? Those are not Rust wins.
The copy in circulation is a machine-syndicated, translated headline. Treat any specific technical detail you see quoted from it elsewhere — versions, crate names, benchmark harnesses — as unverified unless you can trace it to the original article or a first-party post.
Marking inference explicitly — "likely", "appears to", "would need confirmation"
For the rest of this post I'll use three registers:
- Reported — the 36Kr piece says it.
- Likely — my inference from how ports like this usually go wrong, not from their data.
- Would need confirmation — I have no evidence either way and you should not build on it.
Everything I say about their system beyond the three headline claims is inference. Everything I say about mechanisms is testable on your own system, and I'll show you how.
Why a 40% CPU Cut Is Rarely About the Language
Start with what language choice does and doesn't buy you. For a request-serving API, most CPU goes to: TLS, parsing and serialization, allocation and deallocation, syscalls, copying bytes between buffers, and scheduler overhead from concurrency. Of those, only the allocator and parts of the parser are "language" — and the allocator is usually C (jemalloc, mimalloc, glibc malloc) in every runtime involved.
The three levers that actually pay: allocations, concurrency, copies
In practice, double-digit CPU wins in a service rewrite come from exactly three places:
- Fewer and smaller allocations per request, and fewer bytes touched by the allocator and (if present) the garbage collector.
- A concurrency model that stops oversubscribing cores, with bounded queues instead of unbounded ones.
- Fewer full-payload copies at boundaries — fewer trips across the serialize/parse membrane.
Rust helps with all three, but it does none of them for you. clone() compiles. Arc<Mutex<HashMap<..>>> compiles. String everywhere compiles. A first-time Rust team backed by an LLM will produce all three, because they're the shapes that satisfy the borrow checker fastest.
Why "Rust is just faster" explains almost none of the win
A faithful line-by-line transliteration would get you single-digit differences on a typical JSON-over-HTTP service, and possibly a regression in wall clock if the old runtime had a mature, warmed-up JSON path and the new one used a naive derive. Both runtimes end up in the same libc, the same kernel, the same TLS library.
Rust wins decisively when it lets you delete work: no per-request object graph for a GC to trace, no defensive deep copies to dodge aliasing bugs, no thread-per-request stacks, no Box<dyn Error> chains. Those are design changes Rust permits and a GC language penalizes. The compiler isn't emitting faster instructions for the same algorithm; it's making a better algorithm expressible without a review fight.
A table mapping each lever to the metric it moves
| Lever | Primary metric | Secondary effect | How to verify |
|---|---|---|---|
| Allocation count/size per request | CPU (user), GC time if applicable | p99 tail variance | Counting global allocator; heap profiler |
| Concurrency model and bounds | CPU (system + scheduler) | Latency under load | Thread count, pidstat, run-queue length |
| Copies at boundaries | CPU (memcpy, parse) | Memory bandwidth | Bytes touched per request, perf |
| Language/compiler constant factor | CPU, small | — | Same algorithm, both languages, same box |
The bottom row is the one everybody credits. In my experience it's the smallest of the four.
Lever One — Allocation and Ownership Shape
Reconstructing the likely baseline: per-request churn and GC pressure
If the original service ran on a tracing GC, the CPU cost of allocation isn't the allocator call — it's the collection. Throughput-oriented collectors bill you in proportion to allocation rate, and that bill arrives as smeared, unattributable "the service is slow" rather than a line item you can profile easily. Halve the allocations per request and CPU can drop far more than 40% in the collector alone at high load.
Likely, but this needs confirmation: the largest single component of their 40% was allocation-related — either collector pressure in the old runtime, or allocator traffic in the new one that a naive port would have preserved.
Capacity pre-allocation, buffer reuse, and avoiding clone() in hot paths
Three concrete moves, in ascending order of payoff and risk:
- Size up front.
Vec::with_capacity(n),String::with_capacity(n),HashMap::with_capacity(n). Pushing 1,000 elements into a defaultVecreallocates at every growth boundary — O(log n) reallocations and as many memcpys.with_capacity(1_000)is one allocation, zero reallocations. That's arithmetic from the code, not a benchmark. - Reuse buffers across requests. A per-worker scratch
String/Vec<u8>that youclear()and reuse. Cheap in Rust, but it introduces mutable state you have to scope carefully. - Borrow instead of clone in signatures. Accept
&strand&[u8]. ReturnCow<'_, str>when it's sometimes-borrowed. Reach forArc<T>clones rather than deep clones when the data is genuinely shared and immutable.
Arc::clone is not free — it's an atomic increment on a cache line shared by every core touching that Arc. At high request rates against a hot shared config object, that contention is measurable. Prefer &'static or per-worker copies of immutable config.
Reading a port for accidental allocations: collect(), format!, to_string()
AI-generated Rust has a recognizable fingerprint. It compiles, it reads fine, and it allocates like the language it was translated from. The tells:
.map(..).collect::<Vec<_>>()where the vector is immediately iterated againformat!inside a loop to build a larger string.to_string()on values that are only compared or hashedString::fromon literals that could be&'static strBox<dyn std::error::Error>in a handler that needs to match on the error kind.clone()at the top of a function to silence a borrow error
You can rank files by density:
rg --count-matches 'format!|to_string\(\)|to_owned\(\)|\.clone\(\)' src/ \
| sort -t: -k2 -nr | head -20
--count-matches prints path:count per file, so the sort puts the worst offenders first. Treat the ranking as a smell detector, not a measurement — the counts will be specific to your tree.
A before/after code sketch of a request path with and without buffer reuse
This is the pattern I see most often in translated response-building code:
use std::fmt::Write as _;
pub struct Item { pub key: String, pub value: String }
// before: one heap String per item, a Vec of them, then a join
pub fn render_before(items: &[Item]) -> String {
let parts: Vec<String> = items
.iter()
.map(|i| format!("{}={}", i.key, i.value))
.collect();
parts.join("&")
}
// after: one buffer, reserved once, reused across calls
pub fn render_after(items: &[Item], buf: &mut String) {
buf.clear();
let need: usize = items.iter().map(|i| i.key.len() + i.value.len() + 2).sum();
buf.reserve(need);
for (n, i) in items.iter().enumerate() {
if n > 0 { buf.push('&'); }
let _ = write!(buf, "{}={}", i.key, i.value);
}
}Count the allocations by reading, not running: render_before allocates one String per item in format!, plus amortized growth in the Vec, plus the joined output — on the order of 2n + 1. render_after allocates zero in steady state once buf has capacity. If n is 20 items per response at 5,000 req/s, that's roughly 200,000 avoided allocations per second. That arithmetic is the actual claim; go measure whether it matters on your service.
To turn it into a measured claim, use a counting allocator and assert on it in a test:
use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::atomic::{AtomicUsize, Ordering};
pub static ALLOCS: AtomicUsize = AtomicUsize::new(0);
struct Counting;
unsafe impl GlobalAlloc for Counting {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
ALLOCS.fetch_add(1, Ordering::Relaxed);
System.alloc(layout)
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
System.dealloc(ptr, layout)
}
}
#[global_allocator]
static A: Counting = Counting;Wrap the call in a test, snapshot ALLOCS before and after, and assert the delta. Allocation counts are deterministic for a given toolchain and input, unlike timings — which makes them the one performance property you can safely gate in CI.
Assert on allocation counts in unit tests and leave timings to benchmarks that run on dedicated hardware. An allocation-count regression is a real regression; a 3% timing wobble in CI is noise that trains your team to ignore red.
Lever Two — Concurrency Model Changes
Blocking-per-request threads versus bounded pools and async tasks
Thread-per-request is fine until it isn't. Each OS thread carries a stack (commonly 2–8 MB reserved), and more importantly each runnable thread is a scheduling unit. Run 500 concurrent blocking requests on an 8-core box and you have 500 runnable or blocked threads competing for 8 cores: context switches, cache-line migration, and TLB churn all show up as system CPU you can't attribute to any handler.
Moving to a bounded worker pool or an async runtime with a small worker count doesn't make the work cheaper — it makes the scheduling overhead disappear. In a port, this often happens incidentally, and then gets attributed to the language.
Likely for a "core API" refactor: the concurrency model changed alongside the rewrite, and part of that 40% is scheduler overhead that no longer exists.
Lock granularity, Arc contention, and channels instead of shared state
The default translated shape is:
Arc<Mutex<HashMap<String, Thing>>>
One lock over the whole map. Every request takes it. On a many-core machine the lock becomes a serialization point and the mutex cache line ping-pongs between cores. The CPU cost shows up even when the critical section is tiny.
The fixes, roughly in order of preference:
- Don't share. Use channels so a single owner task mutates state and everyone else sends messages. No lock, no contention, and the queue gives you backpressure for free.
- Shard.
Vec<Mutex<HashMap<..>>>indexed by hash prefix. Contention scales down with shard count. - Read-mostly →
RwLockor an immutable snapshot behindarc_swap. - Per-worker state where the data is only used locally.
Backpressure as the hidden latency win, not raw throughput
This is my strongest hypothesis for the 20% latency number. Unbounded queues convert overload into latency: requests wait in an ever-growing line, so p99 climbs while throughput stays flat and CPU looks "busy but not saturated." A bounded channel or a Semaphore converts overload into rejection instead — errors you can see and alert on — and keeps p99 for admitted requests near the service time.
If the new implementation is async-first, it probably got bounded queues as a side effect of using tokio::sync::mpsc or Semaphore correctly, or simply by not having a thread pool queue of depth 10,000. That's a latency win purchased with one design decision, not with a compiler.
Would need confirmation: whether they changed queue bounds at all. If they did, it's the most likely explanation for the latency delta by a wide margin.
The traps a first-time Rust team hits: blocking calls inside async, held guards across await
These are the two failure modes that survive code review and only appear at load.
Blocking inside async. std::fs, std::net, reqwest::blocking, and any parking_lot lock used in an async context will block a runtime worker thread. With N worker threads, N concurrent blocking calls stall the entire runtime. The fix is tokio::task::spawn_blocking for genuinely blocking work, or the async equivalent of the API.
Holding a guard across await.
use std::sync::{Arc, Mutex};
use std::time::Duration;
async fn bump(state: Arc<Mutex<u64>>) {
let mut g = state.lock().unwrap();
*g += 1;
tokio::time::sleep(Duration::from_millis(1)).await; // guard alive across await
}The function compiles on its own. The moment you write tokio::spawn(bump(state)) you get a compile error along the lines of future cannot be sent between threads safely, with a note pointing at the guard — because the resulting future isn't Send. Good news: the compiler catches it. Bad news: it only catches it if you actually spawn across threads; a guard held across an await inside a single-worker runtime, or across a blocking channel send, can deadlock with no compile error at all.
Turn on clippy::await_holding_lock so the lint names the specific guard instead of leaving you to decode a Send error.
Lever Three — Eliminating Copies at the Boundary
Serialization crossings as the dominant copy source
For a JSON API, count the copies per request. Worst case: kernel buffer → userspace buffer → decompressed buffer → parsed owned strings → handler-owned String copies → response String → serialized bytes → kernel buffer. That's six to eight full passes over the payload, and at large payload sizes the memcpy and parse dominate the CPU profile.
The old runtime may have had exactly the same count. If the rewrite reduced it, that's your win — and again, a design decision rather than a language property.
Bytes, &[u8] slices, and borrowed deserialization
Three tools worth knowing:
bytes::Bytes— a reference-counted, cheaply sliceable byte buffer.body.slice(0..16)is a pointer bump, not a copy. Exactly what you want at the network boundary.&[u8]and&strin handler signatures — accepting borrows forces you to keep the buffer alive rather than copying out of it.- Borrowed serde —
#[serde(borrow)]on&'a strfields lets the deserializer hand you slices of the input buffer instead of allocating new strings.
use serde::Deserialize;
#[derive(Deserialize)]
pub struct Event<'a> {
#[serde(borrow)]
pub kind: &'a str,
#[serde(borrow)]
pub subject: &'a str,
pub ts_ms: u64,
}
pub fn parse(buf: &[u8]) -> Result<Event<'_>, serde_json::Error> {
serde_json::from_slice(buf)
}The cost is real: the lifetime infects every downstream function, and Event<'a> can't be stored anywhere that outlives buf. That's the trade. Borrowing a handful of string fields on a high-volume path is usually worth it; borrowing everything turns your service into a lifetime puzzle.
One accuracy note that bites people: serde_json will not hand you a borrowed &[u8] out of a JSON array of numbers. Byte-slice borrowing pays off with byte-oriented formats (postcard, bincode, protobuf-style framing) and with bytes::Bytes at the transport layer, not with base64-in-JSON.
Zero-copy is a boundary decision, not a syntax decision
You cannot "use zero-copy" as a style. It's a decision about buffer ownership at one specific boundary: who owns the bytes, how long the borrow lives, and what has to change downstream to accommodate it.
My rule: pick at most two boundaries where payload bytes genuinely dominate — large request bodies and fan-out responses — and make those borrow. Let every small, low-volume endpoint own its data and stay readable. A port where every handler carries two lifetimes is a port nobody can maintain, and the maintenance cost will eat the CPU savings in incident hours.
What Claude-Assisted Codegen Actually Contributes
Where the model is genuinely useful: mechanical translation and test scaffolding
Credit where it's due. An LLM is excellent at the parts of a port that are large, repetitive, and semantically shallow:
- Converting type definitions and serialization derives across a hundred structs
- Translating an existing test suite case-by-case, fixtures included
- Establishing a handler pattern once, then replicating it consistently
- Plumbing error enums and
Fromconversions - Writing the differential-test harness that compares old and new responses
- Generating
propteststrategies from an existing example corpus
That's the bulk of the typing in a port. Compressing it is a real, bankable productivity gain, and it's the part no senior engineer enjoys.
Where it fails quietly: lifetimes, unsafe blocks, lock ordering, error paths
The failure mode isn't broken code. It's plausible code that compiles and is wrong in a way only load will reveal.
- Lifetime appeasement. The fastest way to satisfy the borrow checker is
.clone(), or'static, or fully-owned types. The model will take that path cheerfully and hand you back the allocation profile of the language you just left. This is the number one way an AI-assisted port silently forfeits its own wins. unsafe. A block that looks right, passes review, and violates aliasing rules. Only Miri or a fuzzer with sanitizers will find it.- Lock ordering and held guards. Deadlocks don't show up in unit tests.
- Error paths.
.unwrap()on request-derived data;Box<dyn Error>erasing the variant you needed to match on;ascasts that truncate wheretry_intowould have failed. - Semantic drift. Integer overflow behaviour, float formatting, sort stability, timezone handling — small behavioural differences that pass a happy-path test and break a downstream consumer.
Why "no prior Rust experience" cuts both ways for review burden
The optimistic reading: a team with no Rust habits has no bad habits to unlearn, and will accept the ownership model's pressure to restructure data instead of fighting it.
The realistic reading: nobody on the team can tell idiomatic from fast, so the model becomes the de facto authority on Rust idiom. That's the wrong reviewer for exactly the properties you care about — allocation shape, concurrency safety, unsafe correctness.
My position: a port like this needs either one experienced Rust reviewer in the loop, or a substitute for taste — hard lint gates, allocation-count assertions in CI, Miri and fuzzing over the unsafe surface, and a load test at production concurrency before the canary. Taste doesn't scale; gates do. Pick one deliberately rather than discovering later that you had neither.
Benchmarking Discipline Before You Believe a 40% CPU Number
Reproduce the baseline on identical hardware and workload
The baseline must be the old service on the same box under the same load, not a remembered number from a dashboard. Pin CPU where you can (taskset, cgroup cpuset), quiet the noisy neighbours, and check that the deployment topology didn't change as part of the port — a new sidecar, a different load balancer, or moved TLS termination will all show up as a "language win."
Measure CPU as core-seconds per request, not wall-clock elapsed
Wall clock tells you latency. CPU tells you cost. The number you want is:
core_seconds_per_request = ((utime_ticks + stime_ticks) / CLK_TCK) / requests_served
Read the counters straight from /proc:
## fields 14 and 15 of /proc/<pid>/stat are utime and stime, in clock ticks
awk '{print "utime=" $14, "stime=" $15}' /proc/"$(pgrep -f target-service)"/stat
Worked example, so the arithmetic is unambiguous: a 60-second run serves 120,000 requests while utime=9000 and stime=1000 ticks at CLK_TCK=100.
- Total CPU:
(9000 + 1000) / 100 = 100core-seconds - Per request:
100 / 120_000 = 0.83 msof CPU per request
A "40% CPU reduction" on that baseline means the target is ~0.50 ms/request. Now the claim is falsifiable, and it's expressed in a unit that maps directly to your bill.
Warmup, steady state, and tail latency at p50/p99
Three things that invalidate most casual comparisons:
- Warmup. A JVM or Node baseline needs seconds to minutes before JIT and inline caches stabilize. A Rust baseline needs allocator arenas to grow and connection pools to fill. Discard the warmup window and say that you discarded it.
- Steady state. Report the measurement window in seconds and the request count. A 10-second sample is a coin flip.
- Tail. Report p50, p95, p99 — and for CPU, report it per percentile bucket if you can. A rewrite that improves mean CPU while making p99 worse has moved work into the tail, which is the worst place for it.
Also: any "20% lower latency" claim without a percentile isn't a claim. p50 and p99 can move in opposite directions within the same change.
A minimal load-generator harness and how to read its output
// usage: TARGET=http://host:8080/ CONC=64 DURATION_MS=60000 node loadgen.mjs
const TARGET = process.env.TARGET;
const CONC = Number(process.env.CONC || 64);
const DURATION_MS = Number(process.env.DURATION_MS || 60000);
const BODY = JSON.stringify({ id: 'abc', n: 1 });
const agent = new Agent({ connections: CONC });
const lat = [];
const deadline = Date.now() + DURATION_MS;
async function worker() {
while (Date.now() < deadline) {
const t0 = process.hrtime.bigint();
const res = await request(TARGET, {
method: 'POST',
body: BODY,
headers: { 'content-type': 'application/json' },
dispatcher: agent,
});
await res.body.dump(); // must consume, or you measure headers only
lat.push(Number(process.hrtime.bigint() - t0) / 1e6);
}
}
await Promise.all(Array.from({ length: CONC }, worker));
lat.sort((a, b) => a - b);
const q = (p) => lat[Math.min(lat.length - 1, Math.floor((p / 100) * lat.length))].toFixed(2);
const rps = (lat.length / (DURATION_MS / 1000)).toFixed(0);
console.log('requests=' + lat.length + ' rps=' + rps);
console.log('p50=' + q(50) + 'ms p95=' + q(95) + 'ms p99=' + q(99) + 'ms');How to read the output, independent of the numbers you happen to get:
| Pattern | Interpretation |
|---|---|
| p99 / p50 < 2 | Flat distribution; service is below saturation |
| p99 / p50 > 5 | Queueing; find the unbounded queue or the lock |
| Throughput flat, latency rising with concurrency | Saturating a resource; you're past the knee |
| Throughput rising, latency flat | Headroom remains; push concurrency higher |
| p50 fine, p99 spiky | GC, allocator, or a periodic task — not the request path |
One caveat you should not skip: a closed-loop generator with fixed concurrency hides latency under saturation, because slow responses reduce the request rate and the generator never measures the requests it would have sent. That's coordinated omission. For the headline comparison, also run an open-loop variant (fixed target RPS, independent of responses) with oha -q <rps> so the latency numbers hold up at the load you actually plan to serve.
Reporting format: command, environment, measurement, result
| Field | Example |
|---|---|
| Command | TARGET=http://10.0.1.7:8080/ CONC=64 DURATION_MS=60000 node loadgen.mjs |
| Environment | c7g.2xlarge, 8 vCPU, 16 GiB, client on separate host, same AZ |
| Measurement | 60 s steady state after 30 s warmup; /proc utime+stime delta; p50/p95/p99 |
| Result | baseline 0.83 ms CPU/req, p99 41 ms at 2,000 rps → new 0.50 ms CPU/req, p99 33 ms |
If a write-up can't fill that table, it's an anecdote. The 36Kr summary fills the "Result" cell and nothing else — which is why this post is an autopsy.
Rollback and Safety Rails for a Port Like This
Shadow traffic and differential testing against the old implementation
Before you move a single user, mirror production requests to the new service and drop the responses. Then diff them, normalized:
- Sort collection fields and map keys before comparison
- Compare floats with a tolerance, not exactly
- Normalize error payloads to
(status, error_code), not message text - Compare the headers you care about, ignore
dateand tracing ids
The output you want is a mismatch rate per endpoint, trending to zero. The output you'll get first is a mismatch on a field you didn't know existed.
Budget for shadow traffic: it roughly doubles backend CPU for the duration. Run it off-peak or on a sampled percentage.
Feature flags, canary percentages, and error-budget gates
A canary should gate on per-endpoint error rate and p99, not on aggregate health checks. And a 1% canary does not exercise the path where your deadlock lives — you need at least one load test at production concurrency pointed at the canary before you trust it. Concurrency bugs are invisible at 1% of traffic; that's exactly the trap.
Gate promotion on an error budget: if p99 or 5xx burn rate on the canary exceeds the baseline by more than the threshold you wrote down before the rollout, revert automatically rather than in a meeting.
What rollback means when the new service has already replaced the old one
This is the question teams skip, and it's the one that turns an incident into an outage.
If the old deploy is gone from your infrastructure, "rollback" means redeploying a stale artifact — against data and wire formats the new service may have already changed. So during the transition:
- Keep the old binary deployable and practice the redeploy once, under load, before you need it.
- Keep shared formats additive — new optional fields and new event types are fine; renames, removals, and type changes are not.
- Don't migrate the datastore (or the primary message schema) until the new path has survived a full traffic cycle.
- Write down the time-to-rollback and treat anything over ~15 minutes under load as "no rollback."
If you can't answer "how long until we're back on the old implementation, right now, while it's broken?" with a number, you don't have a rollback plan. You have a hope.
A Review Checklist for AI-Generated Rust in a Service Path
Blocking I/O inside async, expect()/unwrap() in request handlers
Grep first, then read:
rg -n 'std::fs::|std::net::|reqwest::blocking|parking_lot::Mutex|\.unwrap\(\)|\.expect\(' src/
In an async service, any std::fs or std::net call reachable from a handler is a stall waiting for load. Any .unwrap() on data derived from a request is a panic a client can trigger. Enforce both with lints rather than review vigilance:
cargo clippy --all-targets -- \
-D warnings \
-W clippy::await_holding_lock \
-W clippy::unwrap_used \
-W clippy::expect_used
unwrap_used and expect_used are restriction lints and will fire in tests too; add #[allow] at test-module scope rather than disabling them globally.
Clippy and lint gates, plus Miri for any unsafe block
rustup +nightly component add miri
cargo +nightly miri test --lib decode
## a real report looks like this (paths and line numbers will be yours):
## error: Undefined Behavior: trying to retag from <0x...> for Unique permission
## --> src/decode.rs:88:19Be realistic about scope: Miri can't run real syscalls or most FFI, so it will not run your tokio server. It will run your deserializer, your frame decoder, and any unsafe helper — which is exactly where the AI-generated aliasing bugs live. Point it there.
Fuzzing the deserializer and parser boundary
cargo install cargo-fuzz
cargo fuzz init
cargo fuzz run parse_request -- -max_total_time=600
Properties to assert in the target: no panic, no unbounded allocation (cap input length and, for the allocation-hungry paths, run under Miri), and roundtrip stability where the format is yours. Any parser the model wrote should be fuzzed before it sees production bytes, no exceptions.
Ownership review: who allocates, who frees, who copies
Make this a required field in the PR template for every handler:
allocates: 1 String (response body), 1 Vec<Item> sized from Content-Length
borrows: request body via Bytes; all string fields via &'a str
copies: 0 full-payload copies; 1 small header map clone
Three lines, written by the author, reviewed by a human. It catches the .clone()-to-appease-the-borrow-checker pattern faster than reading the diff, and it makes each handler's performance model explicit instead of implicit.
Open Questions the Report Leaves Unanswered
Was the win measured end-to-end or in a microbenchmark
If 40% came from benchmarking the refactored module in isolation, it may shrink badly in the full service, where TLS, routing, auth, and serialization dominate the CPU profile. A module that's 20% of total CPU can improve by 80% and move the service number by 16%. Without knowing where the measurement boundary sat, the number isn't actionable.
How much of the 40% survives at full production concurrency
Different levers have different load curves:
- Allocation/GC wins grow with load and can shrink to near zero at low concurrency — especially if the baseline had a tracing collector whose cost is superlinear in allocation rate.
- Scheduler wins peak at oversubscription and vanish when you're running 8 threads on 8 cores.
- Backpressure wins only exist above the knee. Below saturation in a closed-loop test, they measure as zero.
Which means a 40% win measured at high concurrency could be 10% at low concurrency, or the reverse. The report doesn't say where on the curve the measurement sat, and after "per what denominator," that's the most important missing number.
Conclusion — Port the Bottleneck, Not the Codebase
Here's my read, clearly labeled as inference: the 40% came from three structural changes — fewer allocations per request, a concurrency model that stopped oversubscribing the cores, and fewer payload copies — and Claude's contribution was making the mechanical 70% of the port cheap enough that the team could afford the structural work. The language was a precondition, not the cause. Had they made the same three changes in the original language, I'd expect a large share of the same win. That's testable on your own service, and it's the cheaper experiment.
So the transferable lesson isn't "use Claude to port to Rust." It's narrower and more useful:
- Port the bottleneck, not the codebase. Profile first. If 60% of CPU is in your JSON path and your allocator, rewriting the other 40% is a hobby.
- Measure in core-seconds per request. Fill in the reporting table or don't quote the number.
- Gate allocation counts in CI. It's the one performance property deterministic enough to assert on.
- Treat the model as a fast typist with no accountability. Budget the review you'd budget for a large PR from an unfamiliar contributor — because that's what it is.
- Keep rollback alive until the new path has survived a full traffic cycle. Concurrency bugs find you after the celebration.
The 36Kr report is a genuinely interesting data point about how fast a cross-language port can now be typed. It is not yet a data point about how much faster Rust is, because it doesn't publish the workload, the hardware, the baseline, or the percentile. Those four fields separate an engineering result from a headline, and no amount of AI assistance fills them in for you.
Further Reading
- 36Kr report (syndicated feed copy) — the source claim being examined
- The Rust Performance Book — profiling, allocations, and bounds checks in practice
std::alloc::GlobalAlloc— the trait behind the counting allocator aboveVec::with_capacity— official docs on pre-allocationtokio::task::spawn_blocking— the escape hatch for blocking work inside async- Tokio tutorial — async runtime model, tasks, and shared state
- Clippy lint list — including
await_holding_lock,unwrap_used,expect_used - Miri — undefined-behaviour detection for
unsafecode - cargo-fuzz — coverage-guided fuzzing for the parser boundary
- Serde: Deserializer lifetimes — how
#[serde(borrow)]actually works bytescrate —Bytes,BytesMut, and cheap slicing at the network boundary- Criterion.rs — statistics-aware microbenchmarks
- tokio-console — runtime task inspection for stuck or starved tasks
- oha — open-loop HTTP load generator for the latency numbers a closed-loop harness hides
More posts

Reproducing the Rust tsc '2x Faster' Claim Across Cold Builds, Incremental Rebuilds, --watch, and tsserver
A hands-on reproducibility check of the claim that an AI-written Rust port of the TypeScript compiler runs 2x faster than the Go-based native port, measuring cold builds, incremental rebuilds, --watch cycles, and tsserver workloads under controlled conditions.

Benchmarking tsrs Against tsc: Testing an AI-Generated Rust TypeScript Compiler Before It Hits Your Build Pipeline
A deep, hands-on benchmark of tsrs, the AI-generated Rust port of TypeScript, comparing its performance and compatibility against tsc and showing how to validate it before adopting it in a build pipeline.

Auditing Reproducibility in Apex-35B, an Agent-Built Qwen3.6 Model
A technical walkthrough of auditing reproducibility in Apex-35B, an agent-built Qwen3.6 model, examining where agentic training loops break down and what applied ML teams should borrow.
Loading comments…