Choosing Rust in 2026 often starts from two sentences. The first: the language is safe by construction. The second: LLMs have removed its historical drawback, the difficulty. The two sentences travel together. They do not describe the same thing.
In its safe subset, Rust prevents bugs that C and C++ let through: use-after-free, double-free, buffer overflow, data race. That gain is measured where new code replaced C or C++. It does not make a CRUD app safer than one already written in TypeScript, Go, or Python. And a model that gets Rust to compile has not, by doing so, chosen the right owner for your data.
The project that shows the gap at scale is called Bun. The older counter-example is called Deno.
What the safety numbers actually measure
In 2019, the Microsoft Security Response Center wrote that about 70% of the vulnerabilities it assigns a CVE come from memory safety, despite reviews and static analysis. The same writing says to pay the complexity of memory management only when it is necessary, and otherwise to prefer a language with a garbage collector.
Android followed that logic on systems code. The share of memory-safety vulnerabilities there went from 76% in 2019 to 24% in 2024, then under 20% in 2025, by writing new code in safe languages, Rust instead of C and C++. Google estimates the density of these flaws in its Rust to be more than 1,000 times lower than in its historical C/C++.
The White House report from February 2024 points the same way, and cites Rust as a safe language suited to systems work. Through the NSA, it also cites C#, Go, Java, Ruby, and Swift as languages that are already memory-safe.
A CRUD app whose bottleneck is the database, the network, and permissions does not buy that class of flaws by moving to Rust. It buys compile times, a thinner ecosystem, and an ownership model someone has to understand. That someone was, historically, the brake. The current pitch declares them gone.
Bun chose Zig because Rust was not productive
Bun is a JavaScript runtime. In 2021, Jarred Sumner wrote it in Zig, alone, in one year, before LLMs. He had tried Rust. He said it plainly afterward: he could not be productive, Zig is fast to learn, and every allocation is visible.
Zig made the project possible. It also let through the class of bugs that the mix of manual memory and JavaScriptCore's garbage collector produces over and over. v1.3.14 was still fixing use-after-free bugs in node:zlib, node:http2, and UDPSocket.send(), double-frees in the CSS parser, and leaks in crypto.scrypt and TLS. The detail is in the July 8, 2026 post.
Rust is a real tool here. Drop runs cleanup when the value dies, instead of a defer that has to be written at every call site. Across 2,000 Bun.build() calls in the same process, the post measures 6,745 MB retained in v1.3.14, and 609 MB once the transition from Zig to Rust is in place. Bun v1.4.4, published on October 22, 2026, is that codebase.
The means used to get there is the other half of the story.
The model was asked to produce Rust without doing Rust
The transition is explicitly mechanical. 535,496 lines of Zig, comments excluded. About 6,500 commits excluding merges, from May 3 to May 14, 2026. The diff that landed is more than a million lines. Up to 64 Claude sessions in parallel, 5.9 billion uncached input tokens, about $165,000 in API cost. Bun had been acquired by Anthropic in December 2025. Jarred Sumner writes it in the same post: a pre-release of Claude wrote most of the transition, and a human watched the loops.
The guide given to the model sets the method:
- the Rust file must follow the Zig structure;
unsafeis acceptable when the Zig already was, with aSAFETYcomment;
- in phase A, do not put a lifetime parameter on a struct;
- do not reach for a raw pointer only to silence the borrow checker.
The safety being promised is the borrow checker's. The writing instruction is to stick to Zig, a language without a borrow checker. The compiler becomes a ticket queue. The post tells the rest: about 16,000 errors, handled crate by crate, until cargo check passed.
The model's first reflex, still according to the post, was to empty the functions that did not compile. Stubs. Then long comments explaining why the workaround was acceptable. The rule added to reviews is one sentence: if the workaround needs a paragraph, the code is wrong.
That is the central absurdity. The model does not bypass the compiler. It reshapes the program until the compiler goes quiet. Green, in this regime, means "no more errors." It does not mean "the lifetime is the right one."
At the time of the post, about 4% of Bun's Rust sits inside an unsafe block: on the order of 13,000 unsafe keywords, across about 27,000 lines, for about 780,000 lines of Rust. 78% of those blocks fit on a single line, a pointer that came from C++ or a call into a C library. Part of that unsafe is the normal price of a runtime glued to JavaScriptCore. Another part is the Zig translated as-is, with the word unsafe made visible. The rewrite sold as the end of use-after-free contains thousands of them, pending a refactor the author places after v1.4.
Four programs that compiled
The examples that follow come from the repository and from the post about the transition. Each one compiled.
The fix for a free is a leak
The first version of the Windows spawn binding dropped a Box at the end of the match. uv_close is asynchronous. Libuv keeps the pointer until the next tick, then the callback frees it. Result: use-after-free, then double-free. The code was clean for the compiler. The fix, still on main, is to leak the Box so it outlives the scope:
spawn::WindowsStdioResult::Buffer(pipe) => { // uv_close is async. Libuv keeps the pointer, then // on_pipe_close reclaims the allocation via heap::take. // Dropping the Box here would be a use-after-free, then a double-free. Box::leak(pipe).close(Subprocess::on_pipe_close)}The repository even adds a shortcut, heap::release, documented as "Box::leak by another name," to reserve a bare leak for data that truly lives for the whole process.
The move is right once a human has named the owner. The path to get there is absurd: the model writes a Box that Rust will drop too early, the compiler accepts it, and the way out of the language is to declare that the memory will not be returned. Without the comment, a reader sees a leak. With the comment, they see a contract with libuv. The model produced the first. The review produced the second.
The assertion that protects disappears from the binary you ship
ZStr::from_buf is a public safe function. Inside it, an unsafe builds a string terminated by a null byte. The check buf[len] == 0 is a debug_assert!. In release, that macro is erased. The comment says the caller guarantees the precondition.
pub fn from_buf(buf: &[u8], len: usize) -> &ZStr { debug_assert!(len < buf.len(), "ZStr::from_buf: NUL must lie within buf"); debug_assert_eq!(buf[len], 0, "ZStr::from_buf: missing NUL at buf[len]"); unsafe { Self::from_raw(buf.as_ptr(), len) }}That is the state of the code on October 30, 2026. A fix that is still open explains that the shipped Zig did this check in ReleaseSafe, and that the transition demoted it to a debug assertion. A wrong len then goes to open, stat, or CreateFileW.
The same gap has already broken visible behavior. In Zig, assert is a function: the argument runs in every build. In Rust, debug_assert! is a macro: in release, the whole expression disappears, including the call it contains. insert_stale, which records a file in the hot-reload graph, stopped running. Hot reload broke. Debug builds passed. Two languages, a line that looks the same, an opposite meaning. The mechanical transition copies the resemblance.
The default percentage panics before it is ignored
In the CSS parser, a color-mix() can omit a percentage. The other side takes the complement. The version that came out of the transition wrote this:
let p1 = first.percentage.unwrap_or(1.0 - second.percentage.unwrap());unwrap_or evaluates its argument immediately. second.percentage.unwrap() runs even when first.percentage is already there. On color-mix(in srgb, red 40%, blue), the second percentage is missing, and the process panics inside the argument that unwrap_or was about to throw away. The fix is unwrap_or_else, which evaluates the closure only when the value is absent. The post cites commit 90111846a14. The parser now uses the lazy form.
The compiler has nothing to say. unwrap on an Option is legal Rust. The absurdity is in the evaluation: the fallback code explodes in the case it is supposed to handle.
A safe API, a dead pointer
PathString::slice presented itself as safe Rust. The SAFETY comment claimed the caller kept the buffer alive. Nothing in the type required it. On May 15, 2026, a report showed the following program, and Miri's output:
let test = Box::new(*b"Hello World");let init = PathString::init(&*test);drop(test);println!("{:?}", init.slice());error: Undefined Behavior: constructing invalid value of type &[u8]:encountered a dangling referenceThe project's bot reproduced the case. The type was deleted on June 2, 2026. The compiler did not reject this API when the model wrote it. An outside tool, run by someone who read the unsafe hidden inside a safe function, did.
The post adds 19 known regressions, since fixed, that came from this syntax-versus-meaning gap. Among them, a filename-interning limit lowered by a constant left as a placeholder (64 instead of the Zig size), until an out-of-bounds access became reachable that Zig release builds did not check the same way. Rust panicked. That is better than an out-of-bounds write. This limit, set too low, comes from the transition, and the test suite eventually hit it.
Deno shows what choosing Rust means
Deno is the other runtime. It started in Go in 2018. In late 2019, the team rewrote it in Rust. Ryan Dahl explains it: Go's garbage collector and V8's got in each other's way. The rewrite, merged in January 2020, replaces the old binding with Rusty V8.
There, the type system does something precise. A V8 Local handle is tied to a scope. Returning it outside that scope does not compile. The example at the top of the binding is that nesting: isolate, HandleScope, context, script. Nobody asked a model to stick to another language while forbidding lifetimes. The lifetimes are the subject.
Even that choice does not make the product safe by magic. Deno.ffi builds a JavaScript buffer on a pointer that came from the caller. The check that comes first is a permission. The block after that trusts the pointer:
permissions.check_ffi_partial_no_path()?;// SAFETY: Trust the user to have provided a real pointer, offset,// and a valid matching size to it.let backing_store = unsafe { v8::ArrayBuffer::new_backing_store_from_ptr( ptr.offset(offset), len, noop_deleter_callback, ptr::null_mut(), )};The phrase "secure by default" means the sandbox, --allow-read, --allow-net, --allow-ffi. That is an architecture decision. The borrow checker does not make it.
What you are actually betting
Bun is the public demonstration that an LLM can take a very large program through the transition to Rust. It holds because the author of the system was in the loop, because the test suite has more than a million assertions, and because the transition was allowed to stay close to the Zig. It does not say that a team which cannot read a lifetime can start its product in Rust "because Claude compiles."
Without that reading, the model will do what Bun's guide had to forbid, then clean up:
- it clones until the borrow disappears;
- it puts
unwrap()in the middle of a chain that looked like it handled errors;
- it wraps state in
Rc<RefCell<T>>orArc<Mutex<T>>, and the panic or the deadlock arrives at runtime;
- it puts
'staticorBox::leakon a value to kill a lifetime;
- it marks a safe function
unsafe, with a comment that repeats the docs without holding the invariant.
These reflexes are described, outside Bun, as typical agent failures. Cloning to silence the borrow checker is a documented anti-pattern. Bun shows the same families at the scale of a runtime: a leak to satisfy a lifetime, an assertion erased from the binary, a safe API whose contract is a comment.
The code that comes out then attracts a specific profile. Someone who can say whether a Box::leak has an owner, whether a debug_assert! still exists in production, whether a SAFETY comment is a proof or a sentence. That profile was the cost the language imposed before LLMs. The model did not remove it. It moved it to review, once the repository is already large.
For a CRUD app, that shift does not buy the drop in flaws measured at Android. It buys a strict compiler and an ownership debt. The flaws that remain, permissions, injection, business logic, are the ones the language does not see. We go through them for generated code in the biggest vulnerabilities in vibe coding and the hidden costs.
Rust remains the right choice when the problem is Deno's, or the one that pushed Bun to leave Zig: a boundary with C or C++, lifetimes the type can express, a memory or latency budget a garbage collector cannot hold. In that case, the team writing the code has to be able to read what the model proposes. The compiler is then an ally. Used as a judge who is shown stubs until he acquits, it produces exactly the programs above.
That complexity has another cost. Lifetimes, borrows, clones, and unsafe blocks take the attention, the reader's and the model's. A wrong business rule, a permission that is too wide, an amount calculated off, ends up buried in the code written to silence the compiler. The model that produced this code is badly placed to see it: its effort went into getting a program that compiles, not into checking that the rule is the right one.
The choice of language comes from the bug you want to make impossible. The LLM comes after, to speed up a team that can still reread the result.
If your repository compiles, leaks, and can only be explained by SAFETY comments, the subject is no longer the prompt. It is taking the code back. AI2H does that restoration work on generated codebases that have lost their shape.