Async and Await

Threads are great for CPU-bound parallelism, but for handling thousands of network connections — each mostly waiting — spawning a thread per connection doesn't scale (recall the C10K problem from the OS series). Async/await is Rust's answer: write code that looks sequential but doesn't block a thread while waiting, letting a handful of threads handle enormous concurrency. Rust's async is powerful and zero-cost, with one distinctive twist — you bring your own runtime to actually run the async code.

This post covers async/await — Rust’s approach to asynchronous, non-blocking concurrency, ideal for I/O-bound work at high concurrency. It covers what async is (and how it differs from threads), async/await syntax, futures, and the role of the runtime. Async is a large topic; this post gives the conceptual foundation and the essential mechanics. It connects to the I/O-models ideas from the OS series — async is how you get high-concurrency non-blocking I/O.

Async vs threads

Async (asynchronous) programming is a different model from threads — non-blocking concurrency suited to I/O-bound work at high concurrency:

Async is a different concurrency model from threads — non-blocking, suited to I/O-bound work at high concurrency (many tasks mostly waiting), where tasks yield instead of blocking a thread while waiting, letting few threads handle many tasks. It’s the async/await model (the OS series’ async I/O). Rust expresses it with async and await.

async and await syntax

Rust’s async syntax centers on async (marking async functions) and .await (awaiting async operations):

// An async function returns a Future; its body doesn't run until awaited.
async fn fetch_data() -> String {
    // ... imagine async I/O here ...
    String::from("data")
}

async fn process() {
    // .await runs the future, yielding while it waits, resuming when ready.
    let data = fetch_data().await;
    println!("Got: {data}");
}

async fn returns a future (its body doesn’t run until awaited), and .await runs a future while yielding if it’s not ready (non-blocking waiting) and resuming when ready — letting you write sequential-looking, non-blocking code. .await (usable only in async functions) is the yield point. Understanding what a future is clarifies the model.

Futures: the foundation

A future is the core async abstraction — a value representing a computation that will complete later:

A future is Rust’s core async abstraction — a lazy, deferred computation that produces a value later, doing nothing until driven (polled) to completion (typically via .await and ultimately a runtime). Futures are lazy (inert until run), advanced by polling, composable, and zero-cost. Because futures are lazy and need polling, Rust async has a distinctive requirement: a runtime.

The runtime: bring your own

Rust’s most distinctive async trait: the language provides async/await and futures, but not a runtime to run them — you bring your own:

Rust’s distinctive async trait is bring-your-own-runtime: the language provides async/await and lazy futures, but you supply a runtime (usually Tokio) to actually run them — a design that keeps async flexible and zero-cost. In practice: async/await + futures + a runtime give high-concurrency non-blocking I/O. Async is a deep topic, but this is its foundation. Next: testing in Rust.

Key takeaways

Further reading

Sources & References

Async in depth