Hardening Rust Code For Production | corrode Rust Consulting
Idiomatic Rust
Hardening Rust Code For Production
by Matthias Endler
Published: 2026-07-21
We talked about patterns for defensive programming in Rust before, in which implicit invariants that aren’t enforced by the compiler lead to utter misery.<br>But being careful isn’t enough!<br>Even valid code can fail at runtime in ways that are hard to predict and control.<br>That’s what we’re covering next.
This article is for you if you want to…
make your code resilient at runtime
harden your Rust code for production
know how Rust code can fail in unexpected ways and how to recover from that
Table of Contents
Click here to expand the table of contents.
Panic Semantics Are Part of Your API
Unwind vs. Abort
Thread-Level vs. Process-Level Failures
Observing Failures With Panic Hooks
Example Panic Hooks
Sanitizing Sensitive Data
Cleanup Operations
Limitations
Stack Overflows And Runtime Behavior
Release and Debug Builds Are Two Different Programs
Testing Release Behavior
Supply-Chain Security
Secure Allocations With mimalloc
Limit Your Runtime Attack Surface
Minimal Docker Images
Filesystem Sandboxing With Landlock
Drop Privileges and Capabilities
Miri: Detect Unsafe Code Issues
Graceful Shutdown Handling
Circuit Breakers for External Dependencies
Resource Limits
Request Body Size Limits
Limit Queue Depth
Set Timeouts on Everything External
Health Checks and Self-Healing
Runtime Hardening Tooling
Panic Semantics Are Part of Your API
What happens when a Rust program panics?<br>There is no single correct answer because panic! is not a “single behavior.”
Unwind vs. Abort
For starters, there’s a difference between unwind and abort.
catch_unwind invokes a closure, which captures the cause of an unwinding panic.
let result = panic::catch_unwind(|| {<br>panic!("oh no!");<br>});<br>But the Rustonomicon has the following to say about unwinding panics:
We would encourage you to only do this sparingly . In particular, Rust’s current unwinding implementation is heavily optimized for the “doesn’t unwind” case. If a program doesn’t unwind, there should be no runtime cost for the program being ready to unwind.
The alternative to unwinding is aborting the entire process. That does what it says on the tin: the program immediately terminates without unwinding the stack or running destructors.<br>Halt and catch fire.<br>Weirdly enough, that’s often the safer choice, especially when dealing with FFI boundaries or performance-critical code.<br>That’s because unwinding across FFI boundaries is undefined behavior, and unwinding can be expensive in performance-sensitive code.
To enable aborting on panic, add the following to your Cargo.toml:
[profile.release]<br>panic = "abort"<br>And even if you did not explicitly configure this, catastrophic panics like stack overflows and out-of-memory errors always abort the process . That’s because unwinding in these situations is unsafe and can lead to undefined behavior.
In practice, this shows up in two places:
Panics that would unwind across an extern “C” boundary are defined to abort instead of unwinding, because letting unwinding cross that boundary is undefined behavior.
And if a malloc fails, it aborts the process. If that’s a problem, you need to proactively check for allocation sizes before allocating or avoid heap allocations altogether.
These failures are fundamentally different from ordinary panics in that they cannot be caught or recovered from.<br>To handle them gracefully, you need to know exactly how and where your program will run, and design accordingly.<br>For example, in the case of malloc, avoid unbounded user input that could lead to excessive allocations.
Thread-Level vs. Process-Level Failures
Another difference is between thread-level failures and process-level crashes.
A common misunderstanding is that panic terminates the entire program, but in a multi-threaded application, that is not necessarily the case.<br>For example, a background worker thread can panic while the main thread continues running.<br>What sounds like a benefit can leave the system in a partially degraded state.
This distinction becomes especially important in long-running systems (servers, workers, async runtimes, …).<br>A panic in a request-handling thread might only abort that one request, while the rest of the service remains available.<br>Here’s a small example using scoped threads (Playground):
use std::{thread, time::Duration};
fn handle_request(id: u32) {<br>println!("request {id}: started");
if id == 2 {<br>panic!("request {id}: handler panicked");
thread::sleep(Duration::from_millis(100));<br>println!("request {id}: finished");
fn main() {<br>thread::scope(|s| {<br>let requests: Vec = (1..=3)<br>.map(|id| (id, s.spawn(move || handle_request(id))))<br>.collect();
for (id, request) in requests {<br>match request.join() {<br>Ok(()) => println!("main: request {id} completed"),<br>Err(_) => println!("main: request {id} failed, but the process is still...