# How Rust interviews really go at defense-autonomy shops

Source: https://www.techinterview.org/post/3233477224/rust-interview-questions-autonomy-systems/
Updated: 2026-08-04 · techinterview.org

The borrow checker rejecting your code is not what fails candidates. Freezing when it does, then reaching for `.clone()` on everything until the red squiggles go away, is exactly the signal these rounds are built to catch. A Rust systems interview at a place like Anduril, Shield AI, Saronic, or Oxide Computer wants to see you restructure ownership on purpose, out loud, when the compiler pushes back. That is a different skill from writing Rust that already compiles, and most people who bomb these loops are strong engineers who never practiced the first one.

These roles have exploded because the autonomy stack moved to Rust. Drone flight software, sensor fusion, sequencers that fan telemetry out to a ground station with a hard latency budget, on-vehicle control loops that cannot afford a garbage-collection pause or a use-after-free. C++ still ships a lot of that code, but new autonomy and infrastructure teams increasingly start in Rust and screen for it directly. If your resume says Rust, expect at least one round where the language itself is the subject rather than only the medium.

## What the loop actually looks like

The shape is consistent across defense-autonomy and systems shops. A recruiter screen, then a technical phone screen or a take-home, then an onsite loop of three or four sixty-minute sessions covering coding, system design, and behavioral. The take-homes at autonomy companies tend to be pointed: build a small real-time stream processor that ingests telemetry from thousands of simulated vehicles, flags anomalies, and emits alerts under a sub-second budget. Three to five hours of work, and they read your concurrency choices as closely as your correctness.

The live coding round is where the language shows up naked. You will get a mid-difficulty algorithm problem, and then the follow-ups turn into ownership and concurrency questions about the code you just wrote. "Now run this across eight worker threads." "What happens if two of them touch this map at once?" "Why does the compiler reject that, and what would you change?" The problem is a vehicle for probing whether you actually understand the model or just memorized the syntax.

## The borrow-checker questions, and the fix they want

The canonical trap is mutation during iteration. Every interviewer has some version of it because it separates people who internalized the aliasing rules from people who fight the compiler by trial and error.


```
let mut items = vec![1, 2, 3];
for x in &items {
    if *x == 2 {
        items.push(*x); // error: cannot borrow `items` as mutable
    }                   // because it's also borrowed as immutable
}
```


The weak answer is "add a clone." The answer they want names the actual rule: you hold a shared borrow for the whole loop, and `push` needs a unique borrow, so the two overlap and that is forbidden. Then you show a real fix. Collect the values you want to add into a separate `Vec` and drain it after the loop, or switch to index-based iteration over the length you captured up front, or use `retain` if the operation is a filter. Naming why beats naming a workaround, every time.

The second common one is returning a reference to something owned by the function, which forces a conversation about who owns what. If a helper builds a `String` and returns `&str` into it, the data dies at the end of the call. The candidate who says "return the owned `String`, or take a buffer by mutable reference and write into it" has understood the point. The candidate who sprinkles lifetime annotations hoping one sticks has not.

## Lifetimes, where strong candidates start hand-waving

Lifetimes are where C++ veterans tend to wobble, because the concept has no C++ analogue you can lean on. Expect to explain why a struct that holds a reference needs a lifetime parameter, and what that parameter actually means. The clean framing: the annotation does not change how long anything lives, it tells the compiler the relationship, that this struct cannot outlive the thing it borrows. You are documenting a constraint the compiler then enforces.

A frequent probe is elision. Why does `fn first(s: &str) -> &str` compile without any annotation? Because the elision rules assign the output the same lifetime as the single input, and the interviewer wants to hear that you know the sugar is there rather than believing lifetimes vanished. The follow-up that catches people is two input references with one output: now the compiler cannot guess, and you have to say which input the result borrows from. If you have ever written `<'a>` on a function without being able to explain what 'a binds to, that is the gap this question finds.

## Send, Sync, and the shared-state question

Once threads enter the picture, the interview turns to the marker traits. `Send` means a value can be moved to another thread. `Sync` means it can be shared by reference across threads, which is the same as saying `&T` is `Send`. You rarely implement these yourself. They matter because the compiler uses them to reject unsound sharing before it can happen, which is the guarantee Rust markets as fearless concurrency.

The question underneath is almost always "how do you share state across threads," and the answer is a decision tree, not a single primitive. `Rc` is not `Send`, so the moment a second thread needs a value you move to `Arc`. `Arc` alone only gives you shared read access, so mutation means wrapping the inside in a lock or an atomic. Knowing which wrapper fits which access pattern is the whole game.

| Type | Shareable across threads? | Reach for it when | Gotcha interviewers probe |
| --- | --- | --- | --- |
| Rc | No (not Send) | Shared ownership on one thread only | Swap to Arc the moment a thread boundary appears |
| Arc | Yes | Shared read-only data across threads | Still immutable; wrap the inside to mutate |
| Arc | Yes | Shared mutable state, low contention | Holding the guard across an .await stalls the task |
| Arc | Yes | Many readers, rare writers | A steady stream of readers can starve the writer |
| Channel (mpsc, crossbeam) | Yes (moves values) | Handing work between threads | Unbounded channels hide backpressure until you run out of memory |
| Atomics (AtomicUsize, …) | Yes | Counters, flags, lock-free structures | Choosing the memory ordering, Relaxed vs Acquire/Release |

A good candidate reaches for a channel before a shared lock when the problem is really "produce here, consume there," because moving ownership through a channel sidesteps the aliasing question entirely. That instinct, preferring message passing to shared mutable state, reads as senior.

## The async trap almost everyone walks into

If the role involves networking or a runtime, you will get async, and there is one mistake so common it is practically the point of the question. You hold a standard-library `Mutex` guard across an `.await`.


```
let data = Arc::new(std::sync::Mutex::new(0));
let mut guard = data.lock().unwrap();
fetch_next().await;   // the guard is still held here
*guard += 1;
```


Two things go wrong and you should name both. The guard from a blocking `Mutex` is not `Send`, so a multi-threaded runtime like Tokio refuses to move the future across threads and you get a wall of compiler text about `Send`. Worse, even where it compiles, you are holding a lock across a suspension point, so the task parks while other tasks pile up behind the lock and throughput collapses. The fix is to keep the critical section tiny: take the lock, mutate, drop the guard, then await. Only if you genuinely must hold state across an await do you reach for `tokio::sync::Mutex`, and you should mention it comes with real cost.

The other async question worth rehearsing is blocking the executor. Someone calls `std::fs::read` or a CPU-heavy loop inside an async task and wonders why unrelated requests time out. The runtime has a small pool of worker threads, one blocked task can wedge a whole worker, and the answer is `spawn_blocking` for the blocking call or a dedicated thread for sustained CPU work. Interviewers at inference and infra companies lean on this because it is the bug their on-call rotation actually sees.

## Lock-free, and when they actually want it

Lock-free comes up hardest at the latency-obsessed end: flight controllers, market-data adjacent systems, anything with a fixed cycle time where a lock's tail latency is unacceptable. Most teams do not want you writing a lock-free queue from scratch in an interview. They want to know you understand atomics and memory ordering well enough to reason about the code, and that you know when to reach for a vetted crate instead.


```
use std::sync::atomic::{AtomicUsize, Ordering};

static DROPPED: AtomicUsize = AtomicUsize::new(0);
DROPPED.fetch_add(1, Ordering::Relaxed);
```


The differentiating question is ordering. Why `Relaxed` for a plain counter, where you only need the increment to be atomic and do not care about ordering relative to other memory? Why `Acquire` and `Release` when an atomic flag publishes data another thread will read, so the write of the data has to be visible before the flag flips? If you can explain that a `Release` store pairs with an `Acquire` load to build a happens-before edge, you are ahead of most candidates, who either default to `SeqCst` everywhere or cannot say what the orderings buy. Defaulting to `SeqCst` is a defensible safe answer, and saying so, then explaining what you would measure before relaxing it, is a stronger answer than pretending you would hand-tune ordering on the spot.

For anything real, name the tooling: `crossbeam` for channels and epoch-based reclamation, an SPSC ring buffer for a single-producer single-consumer telemetry path, `ArrayQueue` when you want a bounded lock-free queue without rolling your own. Reaching for a reviewed implementation over a bespoke one is the mature call, and interviewers read it that way.

## How to prep so it sticks

Write concurrent Rust and let the compiler yell at you, then fix each error by naming the rule you broke rather than by guessing. Keep a running list of the errors you hit twice, since those are the ones an interviewer will find. Practice narrating the borrow checker's objection before you touch the code, because the round is scored on whether you can explain the model, not whether you can eventually satisfy it. If you are targeting the defense-autonomy shops specifically, the [Anduril interview guide](https://www.techinterview.org/companies/anduril-interview-guide/) walks through the broader loop and the mission-fit filter that sits alongside the technical bar.

The engineers who clear these rounds are not the ones with every trait signature memorized. They are the ones who treat a compiler error as information about their design instead of an obstacle, and who can say, without hedging, why the safe version is safe.
