# Why Rust Does Not Use Fibers or Garbage Collection ## The Question Most runtime-heavy languages ship with two things: a garbage collector (Go, Java, Python, C#, Erlang) and fiber-like concurrency (Go goroutines, Erlang processes, Java virtual threads). Rust ships with neither. Why? The answer is the same for both: **Rust pushes the cost to compile time so there is zero cost at runtime.** ## Garbage Collection ### What a GC Does A garbage collector tracks which objects in memory are still reachable from the program. Periodically (or continuously), it scans the heap, finds objects nothing points to, and frees them. ``` Allocate object A Allocate object B A.ref = B // B is reachable through A drop(A) // A is unreachable — GC will free A // B is now also unreachable — GC will free B ``` ### How It Works (Simplified) **Mark-and-sweep** (Go, Java): ``` 1. Pause the program (or run concurrently) 2. Start from "roots" (stack variables, globals) 3. Mark every object reachable from roots 4. Sweep: free every object NOT marked ``` **Reference counting** (Python, Swift, Objective-C): ``` 1. Every object has a counter 2. When a reference is created: counter++ 3. When a reference is dropped: counter-- 4. When counter == 0: free immediately ``` ### The Costs | Cost | Mark-and-sweep GC | Reference counting | |------|-------------------|-------------------| | Pause time | Stop-the-world pauses (Go: ~1ms, Java: varies) | No pauses, but slower per-operation | | Memory overhead | 2x heap needed (live objects + garbage until collected) | Counter per object (8 bytes) | | CPU overhead | GC thread scanning heap (10-30% throughput loss) | Increment/decrement on every pointer operation | | Predictability | Unpredictable latency spikes | Predictable but cycles leak (need cycle collector) | | Cache impact | GC walks heap → cache pollution | Counters spread across memory → cache misses | For a content platform that needs predictable low-latency responses, GC pauses are the enemy. Even Go's ~1ms pauses compound under load — if a GC pause hits during `epoll_wait`, every pending connection stalls. ### What Rust Does Instead: Ownership Rust replaces garbage collection with a compile-time ownership system: ```rust fn main() { let s = String::from("hello"); // s owns the string, allocated on heap let t = s; // ownership moves to t — s is invalid // println!("{}", s); // compile error: s was moved println!("{}", t); // ok } // t goes out of scope → String freed here. Deterministic. No GC. ``` The rules: 1. **Every value has exactly one owner** 2. **When the owner goes out of scope, the value is dropped (freed)** 3. **Ownership can be moved or borrowed, but never duplicated** The compiler enforces these rules at compile time. At runtime, there is: - No GC thread - No mark phase - No sweep phase - No reference counters - No heap scanning - No pauses Memory is freed at the exact point it is no longer needed — deterministically, at the closing brace. ### Lifetimes: The Compile-Time GC References (borrows) have lifetimes — the compiler tracks how long each reference lives and ensures no reference outlives its data: ```rust fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } } ``` The `'a` lifetime annotation tells the compiler: "the returned reference lives as long as both inputs." If you try to return a reference to a local variable, the compiler rejects it — at compile time, not at runtime. ```rust fn bad() -> &str { let s = String::from("hello"); &s // compile error: s is dropped at end of function, reference would dangle } ``` This is what a GC does at runtime (detect unreachable memory). Rust does it at compile time (detect impossible references). Zero runtime cost. ### The Tradeoff | Aspect | GC languages | Rust | |--------|-------------|------| | Developer effort | Low — just allocate, GC handles cleanup | Higher — must think about ownership and lifetimes | | Compile time | Fast | Slower (borrow checker analysis) | | Runtime cost | GC pauses, heap scanning, memory overhead | Zero — deterministic drop at scope exit | | Latency | Unpredictable (GC can pause anytime) | Predictable — no hidden pauses | | Memory usage | 2x+ (garbage accumulates between collections) | Tight — freed immediately when unused | Rust trades developer convenience for runtime performance. For systems software (databases, runtimes, web servers), this is the right trade. ## Fibers ### Why Other Languages Use Fibers Go has goroutines. Erlang has processes. Java 21 has virtual threads. These are all fibers — lightweight user-space threads that the runtime schedules cooperatively (or semi-preemptively in Go's case). They exist because these languages need to: 1. Handle millions of concurrent I/O tasks 2. Let developers write synchronous-looking code (`result = fetch(url)`) that blocks the fiber, not the OS thread 3. Manage scheduling without exposing the event loop ```go // Go: goroutine blocks on I/O — runtime suspends it and runs another go func() { resp, _ := http.Get("https://example.com") // blocks this goroutine, not the thread fmt.Println(resp.Status) }() ``` ### Why Rust Does Not Use Fibers **1. Fibers require a runtime that allocates stacks.** Each fiber needs its own stack (Go starts at 2-8 KB, grows dynamically). This means: - A heap allocation per fiber - Stack overflow checks on every function call - A runtime that manages stack growth and shrinking - Memory overhead proportional to number of concurrent tasks Rust's goal is zero-cost abstractions. Allocating stacks at runtime is a cost. **2. Fibers are hard to optimize across FFI boundaries.** Rust interoperates with C libraries extensively. Fibers with tiny stacks can't safely call into C code (which expects a full OS stack). Go solves this by switching to a system stack for cgo calls — adding complexity and overhead. **3. Async/await achieves the same concurrency without stacks.** Rust's async/await compiles each async function into a state machine — a regular struct stored inline, no heap allocation needed: ```rust async fn fetch_article(title: &str) -> Article { let data = read_from_seg(title).await; // suspend point 1 let html = render_markdown(&data).await; // suspend point 2 Article { title, html } } ``` The compiler transforms this into something like: ```rust enum FetchArticle { Start { title: String }, AfterRead { title: String, data: Vec }, AfterRender { title: String, html: String }, Done, } ``` Each `.await` becomes a variant transition. The "stack" is just the live variables in the current variant — bytes, not kilobytes. No allocation, no stack, no runtime overhead. ### Fiber vs Async/Await: Memory Per Task ``` Go goroutine: ~2,048 bytes minimum (stack) Erlang process: ~2,688 bytes minimum (stack + heap + mailbox) Rust async task: size_of::() — often 32-128 bytes ``` For a million concurrent connections: - Go: ~2 GB just for goroutine stacks - Rust: ~128 MB for state machines (and often less, since the executor batches them) ### When Fibers Would Be Better Fibers have one advantage: **deeply nested call stacks that suspend at arbitrary points.** If a function 20 calls deep needs to yield, a fiber just swaps the stack pointer. With async/await, every function in the chain must be `async` and every call must be `.await`ed — the "async infection" problem. ``` Fibers: yield anywhere in the call stack — transparent to callers Async: yield only at .await points — every caller must be async ``` For database engines with complex query execution plans that suspend mid-evaluation, fibers are compelling. For an HTTP server that suspends at I/O boundaries, async/await is strictly better. ## How This Applies to writeonce writeonce uses neither fibers nor async/await. It uses a plain event loop: ```rust loop { events = epoll_wait(); for event in events { handle(event); // runs to completion, no suspension } } ``` This is the simplest model — no GC, no fibers, no async state machines. Each handler reads from the `.seg` file, renders a template, writes to the socket, and returns. Nothing suspends mid-handler. The memory model: | What | How it's managed | |------|-----------------| | Article data in .seg | Owned by `Store`, freed when `Store` drops | | Template ASTs | Owned by `TemplateRegistry`, live for the process lifetime | | HTTP connections | Owned by `HashMap`, freed on close/hangup | | Subscription table | Owned by `SubscriptionManager`, entries removed on `EPOLLHUP` | No garbage. No fibers. No async. Just ownership, scopes, and the kernel's event notification. The Rust compiler guarantees at compile time that every allocation is freed exactly once, at exactly the right time. ## Summary ``` GC languages (Go, Java): runtime scans heap → frees unreachable objects cost: pauses, memory overhead, CPU overhead Reference counting (Python): counter per object → free at zero cost: per-operation overhead, cycle leaks Rust ownership: compiler tracks ownership → free at scope exit cost: zero at runtime, developer thinks harder Fibers (Go, Erlang): runtime manages stacks → swap on yield cost: stack allocation, stack checks, runtime Async/await (Rust): compiler generates state machines → no stack cost: zero allocation, async must propagate Event loop (writeonce): no tasks, no suspension → handlers run to completion cost: nothing — simplest possible model ``` Rust's answer to both GC and fibers is the same: **make the compiler do the work so the runtime doesn't have to.**