That Number Made Me Rub My Eyes Three Times

I rewrote a Node.js microservice in Rust last week.

Same features, same endpoints, nothing changed. Ran it, and memory usage dropped from 1GB to 40MB.

I stared at the terminal for a few seconds, thinking something was broken.

You might say 1GB must mean bad code on my end. Fair enough, but my Node.js service was reasonably written—using streams where appropriate, releasing objects when done. The problem wasn’t my code. It was the language itself.

GC Isn’t a Free Lunch

JavaScript, Python, Java—they all use garbage collection. You create objects, some background process cleans up the mess later.

Sounds nice? But convenience has a price.

First, GC itself needs memory to run. It needs workspace to do its job, like a janitor needing somewhere to put cleaning supplies.

Second, GC pauses your app at random times. Imagine you’re gaming, then suddenly—freeze for 0.5 seconds—while garbage collection runs in the background.

Third, your garbage doesn’t disappear immediately. Until GC runs, unused data keeps occupying memory. Like your trash can being full, but the janitor hasn’t shown up yet.

I’m not saying GC is bad. For most applications, it’s totally fine—developer experience matters. But if you’re:

  • Running services in the cloud, where every GB costs money
  • Deploying on a Raspberry Pi with limited hardware
  • Building high-performance network services that can’t tolerate random freezes

Then that “free lunch” starts to hurt.

Rust: The Unconventional Path

Rust has no GC. You already know that.

But Rust also doesn’t make you manage memory manually—no need to write free() like in C, no worrying about use-after-free bugs.

So how does it work?

Two words: ownership.

Ownership: Every Value Has a Landlord

In Rust, every value has exactly one owner. Like every house has exactly one landlord.

When that owner “disappears” (function ends, variable goes out of scope), the value gets cleaned up immediately. No waiting for GC, no manual work—just instant cleanup.

fn main() {
    let message = String::from("hello");
    print_message(message);
    // message is gone here, can't use it anymore
}

fn print_message(text: String) {
    println!("{}", text);
} // text gets cleaned up here

When print_message finishes, text vanishes instantly. Memory freed. No waiting, no delay.

This is Rust’s core magic: the ownership system plans everything at compile time, so at runtime you don’t need a “janitor” at all.

Borrowing: Living Without Buying

But what if you just want to use some data without “buying” it?

Borrow it.

fn main() {
    let message = String::from("hello");
    let length = get_length(&message); // borrowing
    println!("Message: {} (length: {})", message, length);
    // message is still usable, just borrowed for a look
}

fn get_length(text: &String) -> usize {
    text.len()
} // just borrowing, original data untouched

Using the & symbol, you can borrow data without taking ownership. When you’re done, the original data stays exactly where it was.

This is what Rust’s borrow checker validates at compile time: anyone borrowing and not returning? Anyone trying to modify while borrowing? Nothing escapes the compiler’s watchful eye.

Stack vs Heap: Rust’s Default Choice

Now for another key distinction: stack vs heap allocation.

Think of the stack as sticky notes on your desk—grab and toss, instant. The heap is like boxes in a warehouse—need to request space, find a spot, store and retrieve take time.

GC languages (like JavaScript) almost always default to heap. Rust does the opposite—put things on the stack whenever possible.

fn example() {
    let x = 42;           // stack: fixed size
    let y = Box::new(42); // heap: dynamic size, boxed
}

This difference creates massive performance gaps. Stack allocation has almost no overhead. Heap allocation needs:

  • Finding a large enough free block
  • Recording where that memory is
  • Freeing it when done

Rust’s “stack when possible” default beats most GC languages on this.

A Side-by-Side Look

Here’s the Node.js version:

const express = require('express');
const app = express();

app.get('/process', (req, res) => {
    const data = new Array(1000000).fill(0);
    const result = data[0];
    res.json({ result });
    // this memory stays until GC runs
});

Here’s the Rust version:

use actix_web::{web, App, HttpServer};

async fn process() -> String {
    let data: Vec<i32> = vec![0; 1000000];
    let result = data[0];
    format!("{}", result)
    // memory freed immediately when function returns
}

The Rust version releases that million-item array the moment the function returns. The Node.js version waits for GC to decide when to run—could be milliseconds, could be seconds.

One request? Not a huge difference. A thousand concurrent requests? The memory gap becomes obvious.

The Cost

Alright, time to talk about the trade-off.

Rust’s ownership system has a steep learning curve. The compiler constantly rejects your code, saying “you can’t do that” over and over. You’ll get frustrated. I did too.

The borrow checker’s rules feel unnatural at first:

  • Can’t borrow mutably while borrowed immutably
  • Borrowed data can’t outlive the original
  • Lifetime annotations can drive you crazy

But once you cross that threshold?

Your code is memory-safe by default. No use-after-free, no data races—the ownership rules have already filled in the gaps for you.

Is It Worth It?

A standard CRUD app? Maybe overkill.

But for:

  • High-concurrency services — every request’s memory footprint matters
  • Embedded systems — extremely limited hardware
  • Latency-sensitive scenarios — can’t tolerate GC pauses
  • Cost-sensitive businesses — every MB saved is money

Rust is absolutely worth the investment.

I’ve seen teams rewrite hot paths in Rust and cut server costs in half. That 1GB to 40MB story isn’t unique.

At the end of the day, Rust’s ownership system plans memory at compile time. Values disappear the moment you stop using them—no waiting for GC to clean up. Borrowing lets you use data without taking ownership, and stack allocation by default beats GC languages’ heap approach. The learning curve upfront is steep, but once you get it, your code is memory-safe by default, saving trouble and money later.

If your project has performance or memory requirements, give Rust a shot.


FAQ

Q: Does ownership and borrowing make Rust code hard to write? A: At first, yes. The compiler is like a strict quality inspector, constantly rejecting code. But those rejections are helping you avoid real runtime bugs. Once you adapt, coding actually becomes faster—you don’t need to worry about “when does this object get freed?”

Q: Does Rust really save that much memory compared to GC languages? A: It depends on the scenario. The difference is huge for memory-sensitive cases: high concurrency, low latency, embedded. When thousands of concurrent requests hit, GC languages might need several GBs while Rust handles it in tens of MBs.

Q: Is migrating an existing Node.js project to Rust worth it? A: Don’t migrate just to migrate. If your current architecture meets performance, memory, and latency requirements, there’s no need to overhaul. But if you’re hitting performance bottlenecks, memory that won’t stop growing, or GC pauses affecting user experience—migrating hot paths to Rust is worth considering.


Have you run into GC-related issues in your projects? Or questions about Rust’s ownership system? Let’s hear it in the comments.