DEV Community

yureki_lab
yureki_lab

Posted on

How I Shipped Production Rust as a TypeScript Dev Using Claude Code

TL;DR

I'm a TypeScript developer. I shipped a production Rust service — a webhook ingestion gateway doing ~1,100 requests/sec at peak — without ever having written Rust professionally, using Claude Code as my translator, tutor, and sparring partner. It worked: p99 latency dropped from 340ms to 14ms and memory went from ~800MB to 38MB. But the scariest part wasn't the borrow checker. It was realizing I was about to deploy code I couldn't fully review. Here's how I handled that, and 5 lessons for coding outside your comfort language with an AI agent. 🦀

The Problem

Our webhook ingestion gateway was a Node.js service (Node 22.x, Fastify) that did three things: verify HMAC signatures, normalize payloads from a dozen external providers, and push them onto a queue. Simple. Boring. And it kept falling over.

At ~800 req/sec, p99 latency climbed past 300ms. The culprit wasn't I/O — it was CPU. HMAC verification plus JSON parsing of some genuinely cursed 2MB payloads pinned the event loop, and Node's answer to CPU-bound work is "have you tried not doing that?" We were running 6 replicas of a service that, on paper, should have needed one.

I'd read enough "we rewrote it in Rust" posts to know the pitch. The problem: nobody on the team wrote Rust. Me included. My Rust experience was one abandoned weekend of fighting the borrow checker in 2023.

But by early 2026 I'd been using Claude Code daily for TypeScript work, and I kept wondering: does an AI agent change the math on "don't rewrite in a language you don't know"?

The constraint that made it interesting: this had to be a real production service with an on-call rotation, not a toy. If it paged someone at 3am, a human had to be able to fix it.

How I Solved It

Step 1: I owned the architecture, the agent owned the syntax

The first thing I did was not ask Claude Code to "rewrite this service in Rust." I've watched agents eat big vague prompts and produce plausible garbage. Instead, I wrote a one-page design doc in plain English: axum for HTTP, tokio for async, one bounded channel between the HTTP handler and the queue publisher, reject-don't-buffer under backpressure.

Then I fed it to the agent piece by piece. A session looked like:

Me: Implement the HMAC verification middleware from the design doc.
    Constant-time comparison. Reject before body parsing if the
    signature header is missing. Here's the TypeScript version
    for reference behavior.
Enter fullscreen mode Exit fullscreen mode

The TypeScript reference turned out to be the killer move. This was my old code:

// Node 22.x — the old gateway
function verifySignature(payload: Buffer, header: string, secret: string): boolean {
  const expected = createHmac("sha256", secret).update(payload).digest("hex");
  const provided = header.replace("sha256=", "");
  return timingSafeEqual(Buffer.from(expected), Buffer.from(provided));
}
Enter fullscreen mode Exit fullscreen mode

And Claude Code (Opus, Claude Code v2.x) produced the Rust equivalent — and immediately flagged a bug I'd shipped years ago: timingSafeEqual throws when buffer lengths differ, which my old code never caught, so a malformed header produced a 500 instead of a 401. The Rust version it wrote handled the length mismatch explicitly:

// Rust 1.88, axum 0.8
use hmac::{Hmac, Mac};
use sha2::Sha256;

fn verify_signature(payload: &[u8], header: &str, secret: &[u8]) -> bool {
    let Some(provided_hex) = header.strip_prefix("sha256=") else {
        return false;
    };
    let Ok(provided) = hex::decode(provided_hex) else {
        return false; // malformed hex → 401, not 500
    };
    let mut mac = Hmac::<Sha256>::new_from_slice(secret)
        .expect("HMAC accepts any key length");
    mac.update(payload);
    mac.verify_slice(&provided).is_ok() // constant-time inside
}
Enter fullscreen mode Exit fullscreen mode

Translating behavior (with a reference implementation) instead of asking for invention kept the agent honest. Every module went the same way: my design, my TS reference where one existed, its Rust.

Step 2: The compiler became my code reviewer

Here's the thing nobody told me about pairing an AI agent with Rust: the borrow checker is an agent babysitter.

In TypeScript, when Claude Code writes subtly wrong code, it compiles, the tests I forgot to write don't catch it, and it detonates in production. In Rust, a huge class of agent mistakes — use-after-move, data races on shared state, forgotten error paths — simply don't compile. The agent's loop became: write → cargo check → read the error → fix → repeat. I watched it fight the compiler so I didn't have to.

Concrete example: the agent's first draft of the queue publisher shared a connection across tasks in a way that couldn't be Send. In Node, the equivalent mistake is a silent race. In Rust, it was a compile error the agent resolved itself in two iterations (by wrapping the producer in the channel design we'd already agreed on).

flowchart LR
    A[Agent writes Rust] --> B[cargo check + clippy]
    B -->|errors| A
    B -->|clean| C[My review: architecture only]
    C -->|design smell| A
    C -->|ok| D[Property tests + load test]
    D -->|regression| A
    D -->|pass| E[Canary deploy]

Step 3: The "I can't review this" problem

Around week two I hit the moment this post is really about. The agent produced a retry module using Pin, a manual Future impl, and lifetime annotations I could not honestly evaluate. It compiled. Tests passed. And I realized: if I merge this, I'm deploying code nobody at the company can review.

That felt unacceptable for production, so I made three rules:

  1. If I can't explain it, it doesn't merge. I told the agent: "Rewrite this without a manual Future impl, even if it costs an allocation. Optimize for a TypeScript dev reading it at 3am." It replaced the whole thing with 30 lines of plain async fn and tokio::time::sleep. The clever version was faster by an amount that did not matter.
  2. Explain-back reviews. For every non-trivial module, I had the agent walk me through the code line by line in a fresh session — fresh matters, because a session that just wrote the code will defend it. The fresh instance found two real issues in its predecessor's work, including a .unwrap() on a queue reconnect path that would have crashed the process during a broker failover.
  3. Property tests as a trust bridge. I couldn't review lifetimes, but I could review properties. We wrote proptest suites asserting things I understood: every payload either lands on the queue exactly once or returns an error to the sender; no input can make the normalizer panic. ~40 lines of proptest caught an integer-overflow edge case in timestamp normalization that 6 years of the Node version never surfaced.

Step 4: Ship scared, but ship canary

We ran the Rust gateway as a canary at 5% traffic for a week, mirroring 100% of traffic to it in shadow mode and diffing outputs against the Node service. The diff harness (also agent-written, in TypeScript — my language, my review) found one real divergence: a provider that sends unsigned test pings that Node accepted and Rust correctly rejected. That was a bug fix, not a regression.

Full cutover after three weeks. Results, same hardware class:

Metric Node 22 (6 replicas) Rust (2 replicas)
p99 latency 340ms 14ms
Memory / replica ~800MB 38MB
Peak throughput ~800 req/sec 1,100+ req/sec (not the limit)
3am pages in first 90 days 0

Lessons Learned

  1. An AI agent doesn't remove the need to know the language — it changes which parts you need to know. I still can't write a lifetime annotation from scratch. I can now read Rust, reason about ownership at the design level, and smell when the agent is overcomplicating. The agent compresses the boring 80% of language learning; you must still buy the load-bearing 20%.
  2. Strong compilers multiply agent reliability. The same agent that quietly ships type-holes through any in TypeScript gets caught red-handed by rustc. If you're picking a language for agent-heavy development, "how much does the compiler catch" should be a first-class criterion. Rust's slogan for agent work is basically if it compiles, the agent probably didn't hallucinate the concurrency.
  3. "Reviewability" is a hard requirement, not a vibe. The most dangerous artifact an agent can produce is code that works and that nobody on your team can evaluate. Set an explicit ceiling: complexity your team can own at 3am, or it doesn't merge. Saying "make it dumber" to an AI is a legitimate engineering decision. ⚠️
  4. Fresh-session review beats self-review. An agent reviewing code it just wrote is a defense attorney. A fresh instance with no attachment found real bugs the authoring session waved through. Costs one extra session; caught a production crash.
  5. Translate behavior, don't request invention. Handing over the old TypeScript as a reference spec grounded every module in observable behavior — and twice surfaced bugs in the original that we'd been running for years. A rewrite is the best audit of the old system you'll ever get.

What's Next

Two threads I'm pulling on:

  • On-call literacy: I'm running biweekly "read Rust with the agent" sessions for the rest of the team, using explain-back on our own production code. The goal isn't to make everyone a Rust dev; it's to make the 3am page survivable for all six of us, not just me.
  • A second service: we're eyeing the payload normalizer's bigger sibling — a report generator with the same CPU-bound profile. Hypothesis: the second rewrite is 3× faster to ship now that the patterns (bounded channels, explain-back, property tests) are established. I'll write that one up too.

Wrap-up

"Don't rewrite in a language you don't know" was good advice when learning the language was the bottleneck. With an AI agent, the bottleneck moves to reviewing a language you don't know — a different problem with different solutions: reviewability ceilings, fresh-session reviews, property tests, canary + shadow traffic.

If you've been staring at a CPU-bound Node or Python service thinking "Rust would fix this, but nobody here writes Rust" — the math has changed. Ship scared, ship canary. 🚀

If this was useful: follow me here on Dev.to — the next post covers whether the second rewrite really was 3× faster. And if you try this experiment yourself, drop a comment; I want to hear where your reviewability ceiling landed. 💬

Top comments (1)

Collapse
 
raju_dandigam profile image
Raju Dandigam

@yureki_lab, the reviewability ceiling is the strongest lesson here. A compiler, property tests, shadow traffic, and fresh-session review each catch a different class of error, but none makes unowned complexity acceptable. Did you record the Node/Rust output divergences as reusable fixtures so the next rewrite starts with an executable compatibility contract instead of rebuilding the shadow comparison?