Rust does not require one universal asynchronous runtime. That does not mean asynchronous work advances without something responsible for polling it and arranging the events it waits for.
Separate language support from operating machinery
An async function gives the caller a future representing work that can be advanced. It does not by itself allocate a thread, register a socket with an operating-system event facility or choose when another task should run. Those are separate responsibilities. Saying Rust async has no runtime can mean that the language does not mandate one particular runtime implementation. Saying an application needs no machinery to drive asynchronous work is a different claim, and usually hides machinery that exists somewhere else in the program.
I find the distinction useful when reviewing dependencies. A small computation that completes immediately may need no general-purpose scheduler. A service handling sockets, timers, task spawning and shutdown needs owners for all of those behaviours, whether supplied by a crate, a host environment or custom code. The absence of a named runtime dependency does not erase those responsibilities. It can mean they are specialized, embedded or distributed across the application, which may be appropriate but should be visible in the design.
One future can finish in one poll
The Future contract returns either a completed value or a pending state, and uses a waker to arrange another opportunity to poll when progress becomes possible. Futures are generally driven by polling; a handle can also represent work progressing independently elsewhere. The illustrative example below polls an immediately ready future using only the standard library. It proves the narrow point that no external runtime is required for this operation. It does not implement waiting, I/O readiness, fairness, timers or a scheduler for multiple tasks.
The no-op waker is valid here because the future is already ready and does not need a later wakeup. Replacing it with a socket operation and repeatedly polling would not magically create a correct executor. A pending operation needs a reliable path from the event that enables progress to a subsequent poll. The example should therefore remain small and honest about its scope. Minimal demonstrations are useful when they isolate a contract; they become misleading when the omitted responsibilities are presented as unnecessary.
use std::future::{ready, Future};
use std::pin::Pin;
use std::task::{Context, Poll, Waker};
fn main() {
let mut future = ready(42);
let mut context = Context::from_waker(Waker::noop());
assert_eq!(Pin::new(&mut future).poll(&mut context), Poll::Ready(42));
}References: [1] Rust standard library: Future
An executor owns opportunities to make progress
The asynchronous Rust book describes an executor as the component that polls top-level futures and schedules them again after wakeups. That division explains why an executor can be small in one environment and substantial in another. A microcontroller main loop, a browser host and a multithreaded server have different constraints. The common requirement is not a particular thread count or queue implementation. It is a reliable relationship between runnable work and the mechanism that gives it another chance to advance.
An illustrative executor with a ready queue needs to handle wakeups that occur while a task is already queued or currently being polled. Blindly enqueuing every notification can create redundant work; dropping a notification at the wrong moment can strand a task. A production design also needs to decide when completed tasks leave the registry and how shutdown interacts with outstanding handles. These are manageable engineering problems, but they are real. Replacing a mature executor with a short loop transfers responsibility for their correctness to the application.
References: [2] Asynchronous Programming in Rust: Build an Executor
A scheduler is not automatically an I/O driver
An executor can know that a future is waiting without knowing what external event will satisfy it. Socket readiness, timer expiry and device completion require integration with their respective event sources. A future from one library may expect a particular driver or thread-local context to exist. Moving it to a different executor does not guarantee that expectation is satisfied. Runtime independence is therefore a property to establish for the specific abstraction, rather than a conclusion drawn merely because its public type implements Future.
I would document an asynchronous library's environmental requirements alongside its API. Does it require a timer facility? Does it spawn background tasks? Must a driver be entered on the calling thread? Can it be used when only a single task is polled? An illustrative pure transformation future might need none of these facilities, while a timeout wrapper necessarily needs a way to observe time advancing. Naming the requirements makes portability concrete and prevents a caller from discovering the dependency only through a panic or a future that never becomes ready.
Await is a suspension opportunity, not a fairness promise
A future that performs a long synchronous computation before returning from poll occupies the thread executing it during that computation. Writing the surrounding function with async syntax does not create preemption inside the calculation. Even an await can complete immediately if the awaited future is ready, so a loop containing many awaits can still run for a long time without giving other tasks a useful opportunity. I would assess the amount of work between actual suspension points rather than count await expressions.
Consider an illustrative single-threaded service that performs 100 milliseconds of parsing in one poll while another task waits to answer a health request. The second task cannot run on that same thread until the first returns control, regardless of how quickly its own logic could finish. Chunking the computation, using an appropriate blocking or CPU work pool, or moving to another architecture are possible remedies. Each has costs in coordination, ownership and ordering. The important step is to recognize the scheduling obligation instead of assuming asynchronous syntax fulfilled it.
Pinning answers a different question
Pinning supplies a contract for values whose correctness depends on their address remaining stable during an address-sensitive part of their lifetime. Compiler-generated future state can need that property. It does not pin a task to a CPU, keep a thread from being rescheduled, prevent every allocation or make polling fair. Those are unrelated uses of the English word pin. I would explain the memory contract separately from execution placement, because combining them produces confident but incorrect claims about how an asynchronous system runs.
Ownership across suspension points still matters. An illustrative future retaining a large buffer while waiting on a small external event can keep that buffer alive for the full wait. A runtime may schedule the task efficiently while memory retention remains excessive. Similarly, dropping a future can abandon local progress without reversing a remote side effect. These are reasons to inspect the state carried across awaits and the cancellation contract of operations, rather than treating the executor choice as the solution to every asynchronous resource problem.
References: [3] Rust standard library: std::pin
Choose the smallest machinery that owns every obligation
A custom executor can be a sensible choice when the environment is constrained and the requirements are narrow, stable and testable. A mature runtime can be a sensible choice when the application needs timers, networking, task management and observable shutdown. I would compare dependency size, operational behaviour, portability and maintenance burden against the actual requirements. Removing a dependency while recreating its difficult parts privately is not automatically simplification. Conversely, importing a large runtime to poll one immediately ready future is not inherently necessary.
My practical review starts with five questions: who polls, who wakes, who detects external readiness, who bounds work and who owns cleanup? The answer can name the same component several times, or different components with clear contracts. If an answer is missing, runtime-free usually means the responsibility has been overlooked. The useful freedom in Rust is the ability to choose and compose those mechanisms. I want that freedom used to make the execution model more explicit, not to turn an omitted crate name into a claim that asynchronous coordination has disappeared.
The same review should include dependencies several layers down. A library advertised as runtime-neutral may expose a generic future while an optional feature introduces timers or task spawning. Test the feature combination actually shipped, and record the execution assumptions at that boundary so a later dependency update cannot silently invalidate the architecture.
Sources and further reading
- Rust standard library: Future
Defines poll, Pending, Ready and wakeup obligations. The one-poll example and responsibility analysis are original illustrations.
- Asynchronous Programming in Rust: Build an Executor
Explains how an executor schedules and polls tasks. The tradeoffs and application examples here do not reproduce its executor implementation.
- Rust standard library: std::pin
Defines the pinning contract for address-sensitive state. The distinction between memory stability and task scheduling is my explanatory interpretation.