Jason McAffee RustEight rewrites, measured

Ditching Node
for Rust

I recently rewrote 8 of my Node services in Rust. In this article I'll be covering some of the significant performance optimizations I was able to achieve with the switch.

Jason McAffee  ·  August 2026  ·  about an 11 minute read

memory reduction
92.5%totalling 1.35 GB
CPU per request
84.1% lower
throughput increase
+567%6.7x, summed over one endpoint per service

01

Why Rust?

I've been a Javascript/ECMAScript advocate since the days of Flash and Jquery. I was impressed by Ryan Dahl's talks, and was an early adopter of Node.js.

With the advent of LLMs and their ability to effectively write code, I've found myself typing less and less code, and focusing more on higher level abstractions, such as ensuring I have good architecture, agentic workflows, and proper testing to ensure my agents are delivering functionality that actually works.

Language and Syntax Matters Less

With that in mind, the appeal of a language that is easy to read and write, such as Typescript, is lessened, and languages that are a bit harder to read and write, but are highly performant, become even more appealing.

Hardware Utilization Matters More

AI build out is driving up the prices of hardware, especially for RAM and GPUs. 128 GB of DDR5 now costs $3500, so as someone who hosts their own services and AI models, I am motivated to optimize my services and applications to utilize as little RAM as possible.

A single small block of mirror-polished steel alone on black granite under a narrow blade of cold light.
Eight production services, 39.94 MiB of binaries. No runtime to install, no node_modules, no virtualenv.

02

A quick tour of the language

A lot of people have never looked at Rust and assume it's C++ with extra punctuation. For the kind of thing I'm talking about here, a CRUD API with a database behind it, it reads a lot like TypeScript where the types are taken seriously and nothing is allowed to be quietly null.

Here's the same code in both languages, for eight of the things we do every day.

Every Rust snippet here is the real shape of the code in these services: axum for routing, serde for request and response bodies, tokio-postgres for the database, etc.

03

Precision gauge blocks wrung together into a single column, the joints between them almost invisible.
Eight services, measured on the same machine, with the same load, before and after.

Where I found the biggest wins

I measured all eight services before the rewrite and after it, on the same machine and with the same load generator, so the numbers below are a like for like comparison rather than an estimate. Here is what changed, in four categories.

RAM

The eight services held 1,444 MB of private memory between them before the rewrite, and 108 MB after, so I got 92.5% of it back. Every single service dropped, and the range was between 54% and 98%, with the AI proxy the largest at 385 MB down to 6.7 MB. There's no garbage collected heap, no JIT, no lazily freed buffers, etc, so a Rust service settles on a number and then stays there.

CPU

Adding up the CPU time each service spends per request, the eight of them went from 15.2 ms to 2.4 ms, which is 84% less. The Media Server was the single biggest drop, going from 12.03 ms to 1.25 ms of CPU to read one 6.8 MB chunk of video off a disk and write it to a socket. Same file, same disk, same network card, so the other 10.8 ms was the runtime carrying those bytes through a garbage collected heap on the way past.

Throughput

Requests per second went up on seven of the eight services. The Media Server's video chunks went from 128 a second to 3,448, the AI proxy went from 10,836 to 108,696, and this site went from 1,426 to 5,880. Summed across one endpoint per service that comes to 6.7x, but the middle service got 2.9x, and that's the more realistic number to expect.

Response times

I benchmarked 18 endpoints on both sides, and p95 came down on 17 of them, by about 86% on average. The interesting part is that the slow requests improved more than the typical ones, so the Media Server's static route went from 16 ms to 3 ms at p50, and from 51 ms to 7 ms at p99. That's what you get when GC pauses and event loop queueing come out of a system, because the typical request was mostly fine already and the worst ones weren't.

04

My services, before and after

I have a variety of services that I host, including a proxy for incoming requests, an AI proxy to centralize multi-modal AI routing, a media server for my Apple TV app, a dynamic DNS updater, static sites, a web service for my synthesizer, and a service manager to manage them all.

Since all of them had good testing with end to end tests, technical design documents for features, and a history of all the work that's been done on them, I was able to fairly easily task Codex Sol on rewriting them, using my typical agent workflow of research, create a TDD, add extensive E2E tests, implement, and verify things work. I was able to rewrite all the services in about 12 hours.

Measured before and after for each rewritten service
Service Memory CPU / request Throughput p95 p99 Reduction in Lines of Code
Edge proxyRoutes every hostname on the box to the right local service −81%−77% −11%14 → 15 ms16 → 17 ms −42%
AI proxyGateway in front of the local LLM stack, streaming and all −98%−5% 10.0x4 → <1 ms13 → <1 ms −35%
Media Server41 routes, Postgres, HLS and byte-range streaming −97%−90% 26.9x86 → 7 ms87 → 7 ms −58%
Service ManagerStarts, stops and supervises the other 33 services −55%not measured 8.3x174 → 22 ms181 → 27 ms −78%
Dynamic DNS updaterReads the router's WAN IP, reconciles 10 Cloudflare records −96%not measured cycle −60%no HTTP surfaceno HTTP surface outlier †
jasonmcaffee.comThis site. Static export, Rust replaced next start −94%−47% 4.1x21.6 → 6.5 ms23.6 → 8.0 ms no server code before
Chordical serverapi.chordical.com: 17 operations over PostgreSQL −94%−32% 2.2x10.7 → 5.3 ms14.3 → 7.2 ms +5%
Media sitemedia.jasonmcaffee.com, plus a range-proxying fallback −90%−77% 3.4x37.3 → 12.4 ms42.8 → 22.8 ms no server code before
All eight −92.5% −84.1% 6.7x summed p95 fell on 17 of 18 endpoints −55%

Memory is private bytes, freshly started on both sides. CPU per request is total process CPU time during a run divided by requests served. Code is hand-written first-party source with tests included, and no dependencies, generated clients or benchmark harnesses counted on either side. Throughput is one representative endpoint per service, so the summed 6.7x is directional, and the median per-service factor of 2.9x is the more defensible number. † The DNS updater went from 156 lines of Python to 1,614 lines of Rust and is left out of the code total, because the Python got its router crypto and Cloudflare client from packages while the Rust implements both directly. The −55% is the other five services.

slowest listener startup
13.3 s → 54 msa restart stopped being an outage
Chordical's deployed runtime
145.7 → 3.9 MiB26,130 files became one
this site's Rust server
571 linesreplaced a 478 MiB runtime
services now on Rust
10 of 33the rest are third-party or shouldn't move

Memory leak found, which underscores the Rust advantage

During the rewrite, I found that my Service Manager had a memory leak, and was eating 3.4 GB of RAM. With the Rust rewrite, the Service Manager's RAM consumption stays steady at 39 MB.

05

Rust rewrites at other companies

Here are the performance gains other companies are reporting when rewriting their products in Rust.

Every card links to the primary source. Where a company published graphs instead of tables I say so, rather than quoting a secondhand multiple.

Cloudflare

NGINX to Rust

70% less CPU

Pingora, the proxy sitting in front of their whole network. Also 67% less memory at the same traffic, while serving over a trillion requests a day.

blog.cloudflare.com

Vite 8

Rollup to Rust

10–30x builds

Rolldown, their new bundler. They published adopter numbers rather than a benchmark: Linear 46 s to 6 s, Ramp down 57%, Beehiiv down 64%.

vite.dev

Bun

Zig to Rust

11 days

They rewrote the entire runtime, 1,009,272 lines changed across 6,502 commits. They did it for memory safety rather than speed, and got idle CPU down 5x and HTTP server memory down 13–48% anyway.

bun.com

Discord

Go to Rust

No more spikes

Their Read States service spiked every two minutes, and it wasn't garbage, it was Go's collector walking a large live heap on a timer. An unoptimized Rust version beat the hand-tuned Go one.

discord.com

Figma

TypeScript to Rust

10x serialization

Their multiplayer server. It also ended the head-of-line blocking where one large document froze the whole worker for everybody else on it.

figma.com

Astral

Python tooling in Rust

80–115x warm

uv, a drop-in replacement for pip, and 8–10x faster cold. Roughly 13% of all PyPI downloads now go through it.

astral.sh

Vercel

Webpack to Rust

Now the default

Turbopack. Vercel says "much faster" without publishing multiples, so take the quoted figures as third-party. The checkable fact is stronger anyway, which is that Webpack is an opt-in flag now.

nextjs.org

ByteDance

Webpack to Rust

5–10x builds

Rspack, about 98% plugin-API compatible, which is the whole point. It's in production at ByteDance, Microsoft, Amazon, Discord and Shopify because migrating takes days rather than months.

rspack.dev

Tailwind

JS to Rust

960ms to 105ms

The Oxide engine, on their own site's full build. The Catalyst UI kit went from 341 ms to 55 ms.

tailwindcss.com

Google

C++ to Rust in Android

76% to under 20%

Memory safety bugs as a share of all Android vulnerabilities, over six years of new code moving to Rust. Rust changes also had a 4x lower rollback rate and spent 25% less time in review.

blog.google

How popular Rust actually is

Rust entered the TIOBE top 10 (a monthly index that ranks languages by how often each one turns up in search results) for the first time in July 2026 at 1.34%, and was at 1.45% in August. It has topped Stack Overflow's most admired ranking every year since 2016, and JetBrains counts 2,267,000 developers using it in the last year, 709,000 of them as their primary language, with 48.8% of organizations reporting non-trivial use.

It isn't going to displace JavaScript or Python by headcount, and 1.45% says so plainly. What's happened is narrower than that, in that Rust has become the default answer for the layer underneath them.

06

Project Versus - Measuring Performance of Rust vs Go vs Node

Rewriting my own services tells me what I got, but it doesn't tell me what the languages are worth against each other, because every one of those rewrites changed the design as well as the language. So we built thirteen implementations of one todo API and made all of them do exactly the same thing.

Same endpoints, same SQL, same 21-field nested JSON byte for byte, same eight pinned CPU cores, same dedicated PostgreSQL 17 cluster, and the same load generator on its own cores. No ORM anywhere. Every arm has to pass a conformance test that compares its responses byte for byte against the others before we let it be benchmarked at all, and every number below is the median of four interleaved repetitions run unattended on an idle machine.

Why three endpoints skip the database

There's a plaintext route, a JSON-out route and a JSON-in route that never touch Postgres. When GET /todos/:id tops out at some number, those three are what tell you whether the ceiling was the framework, the JSON, or the database. Without them you're just measuring PostgreSQL and calling it a language comparison.

Three identical machined metal test coupons laid out in a row on black granite.
Thirteen implementations, one API, one conformance test. Every arm returns the same bytes or it doesn't get benchmarked.

The winner

Rust takes it, and specifically hyper with no framework at all. The best Go was Fiber, and the best Node was plain node:http with a hand-rolled router, which beat both our own hand-optimized Node arm and Fastify.

It came down to CPU and memory rather than throughput, because on the endpoint that matters most, reading one todo by id, all three land within 9% of each other at 28,237, 26,975 and 25,899 requests a second. What separates them is what they spend getting there. Rust used 3.76 cores, Go 4.24 and Node 6.05, and when every implementation is pinned to an identical 10,000 requests a second the same order holds at 1.10, 1.26 and 1.68. Memory is the wider gap, at 2.7 MB idle for Rust against 53.8 for Go and 181.5 for Node, and 26 MB under load against 108 and 844.

private memory, idle
2.7 / 53.8 / 181.5 MBRust / Go / Node
CPU at a pinned 10,000 req/s
1.10 / 1.26 / 1.68cores, out of a budget of 8
deployed artifact
2.4 / 16.7 / 134 MBRust / Go / Node
arms benchmarked
134 Node, 2 Nest, 1 Next, 3 Go, 3 Rust

Every implementation, the harness and the raw results on GitHub