How to Use Waker and Context in Rust Futures

Use Context to register a Waker in your poll method so the executor knows when to resume your future.

The phone number that wakes your code

You are writing a function that waits for a database query. The query takes time. You cannot freeze the whole thread while waiting. You need to pause, let other work happen, and wake up exactly when the result lands. Rust does not use threads for this. It uses a cooperative dance between your code and the executor. The Context is the stage manager handing you a phone number. The Waker is the phone number itself. When the database finishes, someone calls that number, and your code resumes.

Rust's async model is built on polling. The executor asks your future, "Are you done yet?" If you are done, you return the result. If you are not done, you tell the executor how to notify you when you become ready. That notification mechanism is the Waker. The Context is the container that carries the Waker into your poll method.

The restaurant kitchen analogy

Think of a busy restaurant kitchen. You are a waiter. You take an order and hand it to the chef. The kitchen is busy. You cannot stand there staring at the stove. You write down your table number on a slip of paper and give it to the chef. That slip is the Waker. It tells the chef how to notify you. The Context is the clipboard the chef holds, containing the slip and other info about your table.

When the food is ready, the chef looks at the slip, finds your table, and signals you. You come back, pick up the food, and serve it. The poll function is you checking the kitchen. If the food is not ready, you hand over the slip and walk away to take other orders. The executor is the manager running the floor. It checks every waiter. If a waiter has food, they serve it. If not, they hand over their slip and go do something else.

Minimal example: a future that waits

Here is a bare-bones future that checks for data and registers a waker if the data is missing.

use std::task::{Context, Poll};
use std::pin::Pin;
use std::future::Future;

struct PendingRequest {
    // Simulates state that might be updated by an external event
    data: Option<String>,
}

impl Future for PendingRequest {
    type Output = String;

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<String> {
        // Check if the external operation has finished
        if let Some(result) = self.data.take() {
            // Data is ready, return it and complete the future
            return Poll::Ready(result);
        }

        // Data is not ready yet.
        // Extract the waker from the context to register a callback.
        let waker = cx.waker().clone();
        
        // Register the waker with the event loop or OS primitive.
        // This tells the system to call waker.wake() when data arrives.
        register_waker_with_os(waker);

        // Return Pending to tell the executor to pause this future.
        Poll::Pending
    }
}

fn register_waker_with_os(waker: std::task::Waker) {
    // In real code, this hooks into epoll, kqueue, or IOCP.
    // The waker is stored so it can be called later.
    println!("Registered waker for future resume");
}

The poll method returns Poll::Ready when work is complete. It returns Poll::Pending when work is incomplete. Returning Pending is a promise. You are promising the executor that you will wake up later. If you return Pending without registering the waker, you break that promise. The future sleeps forever.

How the waker actually works

The Waker is a smart pointer. It contains a data pointer and a vtable pointer. The vtable holds function pointers for wake, clone, and drop. When you call cx.waker().clone(), you are not copying the entire future. You are incrementing a reference count on the data. The clone is cheap. It costs a few pointer operations.

This design allows wakers to be stored in OS structures. You can hand a waker to epoll on Linux or kqueue on macOS. The OS stores the waker. When an event occurs, the OS calls the wake function pointer in the vtable. That function puts your future back into the executor's run queue.

The Waker is a callback, not a thread. Calling wake does not resume the future immediately. It schedules the future to be polled again. The executor controls when poll runs. This keeps the system responsive and prevents priority inversion.

Realistic example: a timer future

A timer future waits until a deadline passes. It registers the waker with the OS timer facility.

use std::task::{Context, Poll};
use std::pin::Pin;
use std::future::Future;
use std::time::{Instant, Duration};

struct TimerFuture {
    deadline: Instant,
}

impl Future for TimerFuture {
    type Output = ();

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
        // Check if the deadline has passed
        if Instant::now() >= self.deadline {
            return Poll::Ready(());
        }

        // Calculate remaining time
        let remaining = self.deadline - Instant::now();
        
        // Clone the waker to store in the timer registration
        let waker = cx.waker().clone();
        
        // Register the waker with the OS timer
        // The OS will call waker.wake() when the timer expires
        register_os_timer(waker, remaining);

        Poll::Pending
    }
}

fn register_os_timer(waker: std::task::Waker, duration: Duration) {
    // Hooks into the platform timer API
    println!("Setting timer for {:?}", duration);
}

The timer future checks the clock. If the deadline passed, it returns Ready. If not, it calculates the remaining time and registers the waker. The OS handles the waiting. When the timer expires, the OS wakes the future. The executor polls it again. The deadline is now past. The future returns Ready.

Convention aside: cloning wakers

Rust developers clone wakers liberally. The community convention is to call cx.waker().clone() whenever you need to store the waker outside the poll call. Do not fear the clone. It is cheap. The alternative is wake_by_ref, which borrows the waker. Use wake_by_ref only when you can wake the future immediately inside the poll method. If you need to pass the waker to another thread or an OS structure, you must clone it.

Another convention is to keep the poll method small. Do complex logic in helper functions. The poll method should check state, register the waker if needed, and return. This makes the code easier to read and debug.

Pitfalls and compiler errors

Forgetting to register the waker is the most common mistake. You return Poll::Pending but never tell the executor how to wake you. The future is polled once, returns Pending, and never runs again. This is a silent deadlock. The program hangs. The compiler cannot catch this. You must ensure every Pending return path registers the waker.

Registering the waker every time poll is called can be wasteful. If you register the waker when the future is already ready, you might trigger unnecessary wakeups. Check the state before registering. Only register if you are going to return Pending.

Moving out of Pin<&mut Self> causes compiler errors. The poll method takes a pinned mutable reference. You cannot move fields out of self. If you try, you get E0507 (cannot move out of borrowed content). Use Pin::get_mut to access fields safely. Or use self.data.take() if the field is an Option.

If you forget to implement Future, you get E0277 (trait bound not satisfied). The compiler tells you the type does not implement Future. Add impl Future for YourType and define poll.

Do not fight the pinning rules. Pinning exists to prevent self-referential structs from moving. If you need to move data, use Option::take or restructure your state. Treat the Pin as a constraint that keeps your memory safe.

Decision: when to use wakers and context

Use async fn when you can express logic with standard combinators and libraries. The compiler generates the state machine for you. You do not need to touch Waker or Context.

Use Future trait implementation when you are building a low-level adapter for an OS primitive, a custom executor, or a complex state machine that async fn cannot express efficiently.

Use Context and Waker directly only inside poll methods or when integrating with external event loops. Never create a Context manually. The executor provides it.

Use cx.waker().clone() when you need to store the waker in a background thread or OS structure that outlives the poll call. The clone is cheap and safe.

Use cx.waker().wake_by_ref() when you need to trigger a resume immediately without cloning the waker. This is rare and usually only useful for testing or immediate notifications.

Use Poll::Ready when the future has completed its work. Return the result inside Ready.

Use Poll::Pending when the future cannot make progress. Always register the waker before returning Pending.

Register the waker or accept the deadlock. The waker is the lifeline of your async code.

Where to go next