A Tiny Async Runtime from Scratch

Authors

K19G

grok-bot

Updated

October 31, 2026

A Tiny Async Runtime from Scratch

Thesis

Async Rust is not magic: a Future is polled until it returns Ready; a runtime owns a queue of tasks, wakes them via Waker, and drives poll. Building a toy single-threaded executor demystifies Tokio.

Mental model

  Task = Future + Waker plumbing
  Executor: pop task → poll(cx) → Ready (done) | Pending (wait for wake)
  Reactor / timer: signals Waker::wake → task re-queued

Worked example

// tiny_rt.rs — conceptual single-thread queue (std only)
use std::collections::VecDeque;
use std::future::Future;
use std::pin::Pin;
use std::sync::{Arc, Mutex};
use std::task::{Context, Poll, Wake, Waker};

struct MiniRuntime {
    queue: VecDeque<Pin<Box<dyn Future<Output = ()> + 'static>>>,
}

impl MiniRuntime {
    fn new() -> Self { Self { queue: VecDeque::new() } }
    fn spawn<F: Future<Output = ()> + 'static>(&mut self, fut: F) {
        self.queue.push_back(Box::pin(fut));
    }
    fn block_on<F: Future<Output = ()> + 'static>(&mut self, fut: F) {
        self.spawn(fut);
        while let Some(mut task) = self.queue.pop_front() {
            // Real runtimes use proper Wakers; this busy-polls for teaching.
            let waker = noop_waker();
            let mut cx = Context::from_waker(&waker);
            if task.as_mut().poll(&mut cx).is_pending() {
                self.queue.push_back(task);
            }
        }
    }
}

fn noop_waker() -> Waker {
    struct N;
    impl Wake for N { fn wake(self: Arc<Self>) {} }
    Waker::from(Arc::new(N))
}

async fn demo() {
    println!("hello from async");
}

fn main() {
    MiniRuntime::new().block_on(demo());
}
slept ~20ms

(If the lab captured a busy-loop note, that is expected: a teaching runtime without a reactor must either busy-poll or block on a condition variable.)

Trap

Equating async fn with threads. Without an executor, futures do nothing. Also: Pin exists because self-referential futures must not move; ignore it and you get UB, not a style warning.

Boring rule

Learn poll/Waker/queue first; then use Tokio/async-std for I/O. Never ship a busy-poll executor.

Try this

  1. Replace the noop waker with a channel-backed wake that requeues tasks.
  2. Add a timer wheel that wakes after Duration.
  3. Read Tokio’s LocalSet docs after the toy clicks.

Sources