Skip to main content

Rust engineer role-specific interview questions

30 questions on role-specific for rust engineer candidates. Expect prompts such as “Explain Rust ownership and move semantics” and “How does the borrow checker enforce aliasing rules?”, each with a worked answer outline and the follow-ups interviewers push on.

Questions

Showing 1 to 25 of 30 role-specific questions (page 1 of 2).

As asked

Walk me through what happens to a value's ownership when you pass it to a function, and how that differs from languages with garbage collectors. Where exactly does the compiler insert the drop?

Sample answer outline

Strong answer covers the single-owner rule, that passing by value moves ownership into the callee's stack frame, and that the compiler inserts a drop at the end of the owning scope. Candidate should contrast this with GC languages where the runtime tracks reachability at runtime and reclaim asynchronously, noting Rust guarantees zero-cost deterministic cleanup.

Expect these follow-ups

  • What happens if you pass a type that implements Copy instead of moving it?
  • How does the compiler prove at compile time that a moved value is never used again?
ownershipmove-semanticsdropmemory-management

As asked

Describe the aliasing rules the Rust borrow checker enforces. What exactly makes the rule 'either one mutable reference or any number of shared references' safe, and how does the compiler verify it at compile time?

Sample answer outline

Strong answer references the XOR mutable aliasing rule and explains that it eliminates data races at compile time. Candidate should explain the borrow checker tracks borrows as non-overlapping regions, cite iterator invalidation as the classic mutable-alias hazard in C++, and note that the rule enables safe concurrent access patterns like multiple readers.

Expect these follow-ups

  • Can you have two mutable borrows to disjoint fields of the same struct?
  • How does split_at_mut work around the borrow checker?
borrow-checkeraliasingsafetyreferences

As asked

The compiler can often elide lifetimes. When does it fail to do so and require you to write explicit annotations? Give me a concrete function signature where elision would be ambiguous.

Sample answer outline

Candidate should state the three elision rules and explain that elision fails when there are multiple input lifetime parameters and the output lifetime cannot be tied to a single input, or when the compiler cannot determine whether the output should live as long as self or another parameter. A good concrete example is fn longest<'a>(x: &'a str, y: &'a str) -> &'a str showing why the return lifetime is ambiguous without annotation.

Expect these follow-ups

  • What does a lifetime annotation actually constrain at runtime?
  • How do lifetime bounds on structs interact with the struct's own lifetime?
lifetimesannotationselisiongenerics

As asked

You have a function that accepts something implementing Display. When would you use dyn Display and when would you use impl Display or a generic T: Display? What are the concrete runtime and binary-size tradeoffs?

Sample answer outline

Candidate should explain that generics are monomorphized at compile time giving zero virtual-dispatch overhead but potentially bloating binary size, while dyn Trait uses a vtable pointer for dynamic dispatch with one extra pointer indirection per call. Key decision factors are whether you need heterogeneous collections (requires dyn), whether the type is known at compile time (use generics), and whether you care about binary size in embedded contexts.

Expect these follow-ups

  • What is the vtable layout for a trait object in memory?
  • Can you use dyn Trait across threads and why does Send and Sync matter there?
traitsgenericsdynamic-dispatchvtablemonomorphization

As asked

List the specific operations that require an unsafe block in Rust. What does the language guarantee you keep even inside unsafe, and what guarantees does it drop?

Sample answer outline

Five unsafe superpowers: dereference raw pointers, call unsafe functions, access mutable statics, implement unsafe traits, access union fields. Crucially, Rust's type system, ownership, and borrowing still apply inside unsafe for safe code you write there. The language drops only the guarantees that require runtime checks to uphold. Candidate should note that undefined behavior in unsafe can compromise the entire program, not just the unsafe block.

Expect these follow-ups

  • What is the difference between unsafe code and unsound code?
  • How do you write an unsafe abstraction that is sound from the outside?
unsaferaw-pointerssoundnessub

As asked

Explain how the ? operator works under the hood. What trait conversions does it call, and how does that enable you to use ? with multiple error types in one function?

Sample answer outline

? desugars to a match that on Err calls From::from on the error to convert to the function's declared error type, then returns early. This means if you implement From<IoError> and From<ParseError> for your custom MyError, you can ? both in the same function. Candidate should mention anyhow for ad-hoc error aggregation in application code versus thiserror for library errors with defined types.

Expect these follow-ups

  • When would you use anyhow versus thiserror?
  • How do you attach context to an error when using anyhow?
error-handlingfromquestion-markanyhowthiserror

As asked

You find an unsafe block around a C library call in a production Rust service. What invariants do you check before approving the change?

Sample answer outline

The answer should make the safety contract explicit: pointer validity, alignment, ownership, aliasing, lifetime, thread-safety, error handling and cleanup responsibilities. Unsafe should be isolated behind a small safe abstraction whose public API prevents callers from violating those invariants. FFI return codes and null pointers need to become Rust Result or Option values immediately. The candidate should mention tests that exercise boundary cases and tooling such as Miri, sanitizers or fuzzing where practical. A weak answer treats unsafe as a local syntax escape hatch rather than a module-level proof obligation.

Expect these follow-ups

  • How do you document a safety invariant without making every caller read C headers?
  • What is the difference between unsafe to call and unsafe inside?
  • How would you fuzz this boundary?
unsafefficode-review

As asked

If I have a struct that holds a reference to a string slice, what lifetime annotation do I put on the struct and why? What does the compiler actually check at each use site?

Sample answer outline

Candidate should write struct Foo<'a> { s: &'a str } and explain the lifetime parameter binds the struct's valid region to the referent's region. The compiler checks that any Foo instance cannot outlive the data it points to, preventing dangling references. Should also note that this affects what you can do with Foo when you store it in other structs.

Expect these follow-ups

  • How do you express that two reference fields must live at least as long as each other?
  • What happens if you try to return a Foo from a function that owns the string?
lifetimesstructsreferencesborrow-checker

As asked

What is stored in a fat pointer when you create a &dyn Trait or a Box<dyn Trait>? Draw out the memory layout if you had to implement vtable dispatch by hand in C.

Sample answer outline

Candidate should describe a fat pointer as two words: a data pointer to the concrete value and a vtable pointer. The vtable contains the concrete type's size, alignment, a drop glue pointer, and then function pointers for each trait method in declaration order. Should note that Rust vtables are per trait-impl pair, not per type like C++ vtables, and do not support multiple virtual base pointers. Coercion from &T to &dyn Trait causes the compiler to generate the vtable as a static constant. Candidate should also note that trait_upcasting (stabilised in Rust 1.76) now allows coercing &dyn SubTrait to &dyn SuperTrait when the vtable includes supertrait slots.

Expect these follow-ups

  • Why can't you upcast a dyn TraitA to dyn TraitB even if TraitB is a supertrait?
  • How does upcasting work with trait_upcasting feature?
vtablefat-pointermemory-layoutdynamic-dispatch

As asked

When you write async fn fetch_data(), what does the Rust compiler actually produce? Describe the generated type and how await suspension points map to enum variants.

Sample answer outline

Candidate should explain that the compiler generates a state-machine enum implementing Future, where each await point becomes a variant capturing only the locals live across that yield point. The poll method is a match on the current state, calling inner futures and transitioning states. Key insight is that the state machine is stack-allocated by default (when not boxed), which is why async tasks are more efficient than OS threads for I/O-bound work. Should note that the generated future must be pinned before it can be polled, which is why spawning or joining a future requires Pin.

Expect these follow-ups

  • Why do async state machines sometimes require boxing and how does that affect performance?
  • What does Pin<&mut Self> in the poll signature protect against?
asyncfuturestate-machinecompilertokio

As asked

Explain why the poll method on Future takes Pin<&mut Self> instead of &mut Self. What invariant would break if you could move a Future after pinning it?

Sample answer outline

Candidate should explain that async state machines contain self-referential pointers: a field may hold a pointer to another field in the same struct to avoid re-reading a local across an await. If the struct were moved after those pointers were established, they would dangle. Pin guarantees the value's memory address will not change once pinned. Unpin is an auto-trait for types where this guarantee is unnecessary.

Expect these follow-ups

  • When would you manually implement a Future that requires Pin?
  • What does Box::pin do and why does it satisfy the Pin invariant?
pinasyncfutureself-referentialsafety

As asked

Describe how Tokio's multi-threaded runtime picks up tasks and runs them. What is work-stealing and why does Tokio use it? What happens when every thread is blocked in a poll call?

Sample answer outline

Candidate should describe the thread pool where each thread has a local run queue plus access to a global queue and can steal from sibling queues. When a thread exhausts its local queue it steals half the tasks from a random sibling, reducing idle time. Should note that blocking inside poll starves other tasks on that thread and explain spawn_blocking as the escape valve to hand CPU-bound or blocking I/O work to a dedicated blocking thread pool.

Expect these follow-ups

  • What is the difference between tokio::spawn and tokio::task::spawn_blocking?
  • How does Tokio's I/O reactor integrate with the OS epoll/kqueue/IOCP?
tokioasyncwork-stealingconcurrencyscheduler

As asked

You use tokio::select! with two futures and one of them completes first. What happens to the other future? What does 'cancellation safe' mean for a future, and how does the Tokio documentation define it?

Sample answer outline

Candidate should explain that the losing branch's future is simply dropped, freeing all its captured state. A future is cancellation-safe if dropping it at any await point does not lose work or leave external state corrupt. Classic unsafe example is a multi-step write or a future that holds a lock across an await. Candidate should mention tokio::io::AsyncReadExt::read_exact as a known non-cancellation-safe operation.

Expect these follow-ups

  • How would you write a cancellation-safe version of an operation that normally is not?
  • What is the difference between cancelling a future and aborting a JoinHandle?
tokiocancellationselectasyncsafety

As asked

By default, what does Rust do with struct field ordering and padding? How does #[repr(C)] change that, and when would you need it in practice?

Sample answer outline

Rust's default repr may reorder fields to minimize size and is not stable across compiler versions. #[repr(C)] fixes field order to declaration order with C-compatible alignment and padding, making layout predictable. Needed when doing FFI with C libraries, writing to binary file formats, or using mem::transmute across type boundaries. Candidate should mention #[repr(packed)] and its alignment hazards.

Expect these follow-ups

  • What is the size of a typical Rust enum wrapping a u64 vs a C-style enum?
  • When would you use #[repr(u8)] on an enum?
memory-layoutreprffialignmentstructs

As asked

Describe each of tokio's four channel types: mpsc, oneshot, broadcast, and watch. Give me a concrete scenario where you would pick each one.

Sample answer outline

mpsc for fan-in work queues where many producers send to one consumer. oneshot for a single response from a spawned task back to its spawner, like a request-response pair. broadcast for fan-out where every subscriber needs every message, such as config change notifications. watch for the latest-value-wins pattern where readers only care about the current state, like a health check or config snapshot. Candidate should note broadcast has backpressure only through lagging semantics.

Expect these follow-ups

  • What happens to broadcast messages when a receiver lags behind?
  • How would you implement a request-response pattern over a single mpsc channel?
tokiochannelsconcurrencyasyncmpsc

As asked

You have two threads each holding an Arc<Mutex<A>> and an Arc<Mutex<B>>. Thread 1 locks A then tries to lock B; Thread 2 locks B then tries to lock A. Walk me through what happens and how you fix it.

Sample answer outline

Classic deadlock via lock-order inversion. Fix options include: enforce a global lock-ordering convention so all threads acquire locks in the same order; combine A and B into a single Mutex<(A, B)>; or use a try_lock with backoff. Should also mention parking_lot::Mutex for better performance than std::sync::Mutex and its lack of poisoning semantics.

Expect these follow-ups

  • What is mutex poisoning in std::sync and when would you recover from it?
  • How does RwLock differ from Mutex for read-heavy workloads?
concurrencymutexdeadlockarctokio

As asked

How should error type design differ between a library crate and an application binary? What problems arise if a library exposes anyhow::Error at its API boundary?

Sample answer outline

Libraries should expose structured error types (typically via thiserror) so callers can match on specific variants programmatically. Exposing anyhow::Error erases type information and forces callers to parse error messages, making downstream error handling fragile and coupling to implementation details. Applications can use anyhow freely since they are the terminal consumer. Should mention the std::error::Error trait as the cross-crate compatibility layer.

Expect these follow-ups

  • How do you add error context without losing the original cause?
  • What is the source chain and how does std print it?
error-handlingthiserroranyhowapi-designlibraries

As asked

Your Rust service is using more CPU than expected in production. Walk me through the tools and process you would use to identify the hotspot, from flamegraph generation to reading the output.

Sample answer outline

Candidate should mention cargo-flamegraph or perf on Linux with DWARF debug symbols, setting debuginfo = 1 in release profile to keep symbol names without disabling optimizations. Reading a flamegraph: wide bars are expensive, tall stacks show call depth. Should also mention criterion for micro-benchmarks to isolate a hypothesis and DHAT or valgrind's massif for allocation profiling.

Expect these follow-ups

  • How do you profile async Rust where tasks are multiplexed on threads?
  • What does debuginfo = 1 vs 2 give you and what is the binary size cost?
performanceprofilingflamegraphdebuggingcriterion

As asked

Rust iterators are described as zero-cost abstractions. What does that mean concretely? Show me why calling .filter().map().collect() on a Vec can produce the same assembly as a hand-written loop.

Sample answer outline

Each iterator adapter is a struct with state, and the entire chain is resolved at compile time through monomorphization and inlining. The compiler sees through the layer of closures and structs to produce a single loop with no heap allocation. Candidate should contrast this with Java streams or Python generators which carry overhead from dynamic dispatch or interpretation. Should note that the Rust compiler relies on LLVM's optimization passes for final loop fusion.

Expect these follow-ups

  • When does an iterator chain allocate and how do you avoid it?
  • What is the difference between Iterator and IntoIterator?
iteratorszero-costperformancegenericsllvm

As asked

Explain where unit tests and integration tests live in a Rust project. What can an integration test access that a unit test cannot, and when would you choose each?

Sample answer outline

Unit tests live in the same file as the code under #[cfg(test)] modules and can access private items. Integration tests live under tests/ and only have access to the public API, making them better for verifying the crate as a black box. Should mention that integration tests for a library crate each compile as a separate binary. Should also cover doc-tests and the -p flag for running tests in a workspace.

Expect these follow-ups

  • How do you share test helpers between integration test files?
  • What does cargo nextest offer over the built-in test runner?
testingunit-testsintegration-testscargo

As asked

Describe how the tracing crate differs from the log crate. How do spans and events work together, and what do you gain by using tracing in an async Tokio service versus plain log macros?

Sample answer outline

tracing introduces spans which have a lifetime and can be entered/exited, making them async-aware: a span attached to a task follows the task across await points automatically. Events are point-in-time log messages that inherit context from the current span. In async code this means logs automatically carry request IDs or user IDs from an outer span, which plain log macros cannot do since they have no context threading.

Expect these follow-ups

  • How do you export tracing data to an OpenTelemetry collector?
  • What is the subscriber model in tracing and how do you compose subscribers?
tracingobservabilitytokioasyncopentelemetry

As asked

You have a mono-repo with a binary crate, a library crate, and a proc-macro crate. How do you structure this as a Cargo workspace and what problems does the workspace Cargo.toml solve for shared dependencies?

Sample answer outline

Workspace Cargo.toml lists all member crates; cargo builds the whole graph and caches shared compilation artifacts. The [workspace.dependencies] table lets you pin a dependency version once and reference it with workspace = true in member Cargo.tomls, preventing version drift. Should mention that proc-macro crates must be their own crate compiled for the host, and that cargo test/clippy can be run across all members in one command.

Expect these follow-ups

  • How do you publish a workspace to crates.io where crates depend on each other?
  • What is cargo hakari and why might you want it in a large workspace?
cargoworkspacebuilddependency-management

As asked

Walk me through using sqlx to run a parameterized query against Postgres in an async Rust service. What does the compile-time query checking buy you and what does it cost?

Sample answer outline

sqlx::query_as! macro sends the query to a real database at compile time (via DATABASE_URL), verifies column names and types, and generates a typed row struct. This eliminates an entire class of runtime type mismatch errors. Cost is that you need a live database or offline snapshot (sqlx prepare) during CI. Candidate should cover connection pooling with PgPool, the acquire pattern, and transaction handling.

Expect these follow-ups

  • How do you handle database migrations with sqlx?
  • What is the difference between sqlx::query! and sqlx::query_as! ?
sqlxpostgresasynctokiodatabases

As asked

You need to serialize a Rust enum where each variant must map to a specific string key that does not match the variant name, and the JSON must omit None fields. How do you configure serde to handle this without writing a full manual Serialize impl?

Sample answer outline

Use #[serde(rename = "key")] on each variant, #[serde(skip_serializing_if = "Option::is_none")] on fields, and #[serde(rename_all = "snake_case")] for global renaming. Candidate should also mention #[serde(tag = "type")] for internally tagged enums and when you would fall back to impl Serialize manually, such as for heterogeneous binary formats or when the field structure depends on runtime data.

Expect these follow-ups

  • How do you deserialize a field that may be either a string or an integer?
  • What is the serde data model and why does it matter for writing a custom serializer?
serdejsonserializationderiveapi

As asked

You are reviewing a Rust codebase with several unsafe blocks written by a former engineer. You suspect one of them may have undefined behavior but you are not certain. What do you do?

Sample answer outline

Candidate should describe a systematic approach: run Miri (cargo miri test) to catch UB the sanitizer can detect, add SAFETY comments explaining the assumed invariants, write tests that stress the code paths under Miri, and consider running cargo-fuzz or proptest to hit edge cases. Should also mention that if Miri cannot run the code (FFI) they would isolate the unsafe boundary and add assertions around the invariants.

Expect these follow-ups

  • What does Miri actually check and what are its limitations?
  • How do you write a SAFETY comment that is useful rather than just ceremonial?
unsafeubmiridebuggingsoundness