Hardening Rust Code for Production

vsgherzi1 pts0 comments

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...

panic rust code request unwinding thread

Related Articles