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.
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
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.
| 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 ms | 16 → 17 ms | −42% |
| AI proxyGateway in front of the local LLM stack, streaming and all | −98% | −5% | 10.0x | 4 → <1 ms | 13 → <1 ms | −35% |
| Media Server41 routes, Postgres, HLS and byte-range streaming | −97% | −90% | 26.9x | 86 → 7 ms | 87 → 7 ms | −58% |
| Service ManagerStarts, stops and supervises the other 33 services | −55% | not measured | 8.3x | 174 → 22 ms | 181 → 27 ms | −78% |
| Dynamic DNS updaterReads the router's WAN IP, reconciles 10 Cloudflare records | −96% | not measured | cycle −60% | no HTTP surface | no HTTP surface | outlier † |
jasonmcaffee.comThis site. Static export, Rust replaced next start |
−94% | −47% | 4.1x | 21.6 → 6.5 ms | 23.6 → 8.0 ms | no server code before |
| Chordical serverapi.chordical.com: 17 operations over PostgreSQL | −94% | −32% | 2.2x | 10.7 → 5.3 ms | 14.3 → 7.2 ms | +5% |
| Media sitemedia.jasonmcaffee.com, plus a range-proxying fallback | −90% | −77% | 3.4x | 37.3 → 12.4 ms | 42.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
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.
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