While learning how Tokio polls tasks, I kept meeting this type:
Pin<&mut Self>
At first, Pin looked like an async-specific smart pointer. It is neither. Pin is a general memory-safety contract for values whose correctness can depend on staying at one address. Async Rust is simply where I encounter that contract most often.
The easiest path for me is to build the idea in order:
- Rust values normally move.
- An async function becomes a future state machine.
- That state machine may need to refer to its own stored values.
- Such a future must not move while it is being polled.
Pinexpresses that promise;Unpinidentifies values that do not need it.- Stack versus heap decides where the value is pinned.
- Static versus dynamic dispatch decides how its concrete type is called.
Those last two decisions are independent. Keeping them separate removes most of the confusion.
01 · Moving values
A Rust move may change a value's address
Rust moves values during assignment, function calls, returns and collection operations.
let first = String::from("hello");let second = first;
Ownership moved from first to second, so first can no longer be used. At the language level, Rust does not promise that the String value itself remains at the old address.
The compiler may optimize away physical copies. A String also owns a separate heap buffer, so moving the three-word String handle does not normally move that buffer. Neither detail gives arbitrary values an address guarantee. Safe Rust assumes ordinary values are movable unless an API establishes a pinning contract.
For most values, moving is harmless:
Before move After move 0x1000: Counter { value: 7 } 0x1000: old location unusable 0x2000: Counter { value: 7 }
Counter means the same thing at either address. It does not contain anything that depends on 0x1000.
The problem begins when a value stores a pointer to a part of itself. Then its address becomes part of its correctness.
02 · Async state machine
An async function becomes a state machine
Calling an async fn creates a future. The function body progresses when an executor polls that future.
async fn download() -> usize { let response = open_connection().await; let body = read_body(response).await; body.len()}
The compiler creates a hidden concrete type that remembers what must survive each suspension point. Conceptually—not as exact generated Rust—it resembles an enum:
enum DownloadFuture { Start, WaitingForConnection, WaitingForBody { response: Response, }, Done,}
Each .await is a possible pause:
create future ↓poll Start ↓connection not ready ──→ Pending ──→ wake later ↓ poll again ↓body not ready ────────→ Pending ──→ wake later ↓ Ready(length)
The real generated type can store local variables, awaited child futures and a state discriminant. Its exact layout is a compiler implementation detail. The important fact is that values used after an .await must be kept inside the future so they are still available when execution resumes.
03 · Address sensitivity
A future may become address-sensitive
Consider a local reference that remains live across an .await:
async fn print_after_wait() { let message = String::from("hello"); let borrowed = &message; wait_once().await; println!("{borrowed}");}
When this function is suspended, the future must retain both message and the information represented by borrowed. Conceptually, the resulting state can point from one stored field into another:
Future at address 0x1000 ┌──────────────────────────────────┐│ message: "hello" ││ borrowed ───────────────────┐ ││ state: waiting │ │└────────────────────────────┼────┘ └──→ message inside this future
If that address-sensitive state were copied to 0x2000 without repairing the internal relationship, the saved pointer could still refer to the old location.
move bytes to 0x2000 ↓new future contains an old-address pointer ↓resume and dereference it ↓invalid memory access
Not every future actually contains a self-reference. Rust nevertheless needs one Future interface that is safe for futures that do. Compiler-generated async futures therefore commonly do not implement Unpin.
The timing is also important:
- A future may be moved freely before it is pinned.
- It must be pinned before it is polled through the
Futureinterface. - Once a non-
Unpinvalue is pinned, it must remain at that address and valid there until its destructor runs.
It is not the first .await that magically pours concrete around the value. The pinning pointer establishes the contract before polling begins.
04 · Pin
What Pin actually guarantees
The core Future method is:
trait Future { type Output; fn poll( self: Pin<&mut Self>, cx: &mut Context<'_>, ) -> Poll<Self::Output>;}
poll does not receive an ordinary &mut Self. It receives a pinned mutable reference. For a future that does not implement Unpin, safe code cannot turn that reference into an unrestricted &mut Self and then use operations such as mem::replace to move the future out.
Pin<Ptr> pins the pointee, not the pointer object:
Pin<Box<MyFuture>> │ │ │ └── Box handle may move │ └────── MyFuture allocation must stay put
Moving a Pin<Box<MyFuture>> variable is therefore safe. The small box handle moves, while the future in the heap allocation stays at the same address.
Pin does not:
- allocate memory;
- freeze all fields or make the value immutable;
- lock a thread or operating-system page;
- make a type
Sendor thread-safe; - prevent unsafe code from breaking the promise.
It is a library contract enforced at safe API boundaries. Correct unsafe implementations uphold that contract; safe callers then cannot accidentally move a pinned, address-sensitive value.
05 · Unpin
Unpin means moving is harmless
Unpin is an auto trait. Most ordinary Rust types implement it automatically because their validity does not depend on their address.
Examples normally include numbers, references, String, Vec<T> when appropriate, and regular application structs composed of Unpin fields.
For T: Unpin, pinning adds no movement restriction:
let mut name = String::from("Ada");let pinned = Pin::new(&mut name); let ordinary: &mut String = Pin::get_mut(pinned);ordinary.push_str(" Lovelace");
This is safe because moving a String value does not invalidate the String itself.
A type can opt out of automatic Unpin, commonly with PhantomPinned, when its implementation relies on a stable address:
use std::marker::PhantomPinned; struct AddressSensitive { data: String, pointer_into_data: *const str, _pin: PhantomPinned,}
That snippet only shows the shape of such a type. Correctly initializing its internal pointer, projecting pinned fields and implementing Drop require careful unsafe reasoning. In normal application code I avoid hand-writing self-referential structs and use a well-reviewed abstraction when one is really necessary.
The memory rule is:
T: Unpin → moving T is always safe; Pin is mechanically removableT: !Unpin → once pinned, safe code must not move T until drop
I should not “fix” an Unpin compiler error by manually implementing Unpin unless I can prove the type has no address-sensitive invariant.
06 · Stack vs heap
Stack pinning versus heap pinning
Pinning does not require the heap. It requires a pointer whose pointee can remain at a stable address for the required lifetime.
Stack pinning with pin!
The standard pin! macro can pin a local value for the rest of its scope:
async fn run_locally() { let future = do_work(); let pinned = std::pin::pin!(future); let result = pinned.await; use_result(result);}
The value stays in this stack frame. The pinned reference cannot escape beyond the lifetime of that local storage.
current stack frame┌─────────────────────────┐│ future at stable address│←── Pin<&mut Future>└─────────────────────────┘scope ends → future drops
Stack pinning is useful when the future is local and short-lived. It avoids a heap allocation.
Heap pinning with Box::pin
Box::pin allocates a value and returns a pinned owning pointer:
let pinned: Pin<Box<_>> = Box::pin(do_work());send_to_executor(pinned);
stack heap┌──────────────────┐ ┌──────────────────────┐│ Pin<Box<_>> handle├───────→│ future: stable address│└──────────────────┘ └──────────────────────┘ handle may move pointee may not move
Heap pinning is useful when ownership must escape the current scope, be stored in a collection, or be type-erased behind dyn Future.
The short comparison is:
| Choice | Owns the future? | Heap allocation? | Typical lifetime |
|---|---|---|---|
Pin<&mut F> from pin! | No | No | Current scope |
Pin<Box<F>> from Box::pin | Yes | Yes | Can be moved and stored as an owning handle |
07 · Static vs dynamic
Static versus dynamic futures
Static and dynamic here describe dispatch, not memory location and not lifetimes.
Static dispatch: one concrete future type
Every async function or async block has a concrete compiler-generated future type. Its name is hidden, but the compiler knows its size and implementation.
async fn load_user(id: u64) -> User { query_user(id).await}
Conceptually, async fn returns an unnamed concrete type implementing Future:
fn load_user(id: u64) -> impl Future<Output = User> { async move { query_user(id).await }}
impl Future means “one specific type chosen here, even though its name is hidden.” The compiler can monomorphize generic callers and often inline through the future's poll implementation.
When runtime branching fits inside one outer async block, I can keep static dispatch:
fn load_user(id: u64, use_cache: bool) -> impl Future<Output = User> { async move { if use_cache { read_cache(id).await } else { query_user(id).await } }}
There is one outer async block, so there is one concrete returned future type. Its state machine contains the branch.
Dynamic dispatch: erase different future types
Two separate async blocks have two different concrete types, even when both return the same output. If an API must return either type directly, store mixed futures in one collection, or hide implementations behind a trait boundary, I can erase the concrete type:
use std::future::Future;use std::pin::Pin; type UserFuture = Pin<Box<dyn Future<Output = User> + Send + 'static>>; fn load_user(id: u64, use_cache: bool) -> UserFuture { if use_cache { Box::pin(async move { read_cache(id).await }) } else { Box::pin(async move { query_user(id).await }) }}
Each branch creates a different future type. Both are placed behind the same dyn Future interface.
There are three separate pieces in that return type:
Pin< Box< dyn Future<Output = User> + Send + 'static > >│ │ │ │ ││ │ │ │ └─ no short-lived borrows│ │ │ └──────── can move between threads│ │ └───────────────────────────────────── runtime-dispatched interface│ └────────────────────────────────────────── owns unsized value on heap└─────────────────────────────────────────────── pointee cannot be moved
A trait object such as dyn Future is dynamically sized, so it must be used behind a pointer. The pointer carries a data address and vtable information. Polling performs an indirect call through that vtable.
Box solves ownership and unknown size. Pin solves address stability. dyn Future solves type erasure. They are often used together, but they solve three different problems.
| Property | Static: impl Future | Dynamic: dyn Future |
|---|---|---|
| Concrete type known to compiler | Yes | Erased at boundary |
| Dispatch | Direct/monomorphized | Indirect through vtable |
| Heap allocation required by dispatch | No | Usually Box or another owning pointer |
| Different future types in one collection | Not directly | Yes, behind one trait-object type |
| Optimization opportunity | Usually greater | Usually less across the erased boundary |
| API flexibility | One concrete returned type | Runtime-selected implementations |
Dynamic dispatch is not automatically slow, and static dispatch is not automatically best. The choice depends on whether allocation, binary size, compile time, API stability and heterogeneous storage matter on that path.
'static is not static dispatch
The 'static bound in dyn Future + 'static is about borrowed data. It says the future does not contain a borrow that can expire before the future is dropped. An async block that owns its captured values can satisfy this bound and still complete immediately.
It does not mean:
- the future runs forever;
- the future lives in static memory;
- dispatch is static;
- the future is pinned.
Similarly, Pin<Box<MyConcreteFuture>> uses static dispatch even though it is heap allocated, while Pin<Box<dyn Future>> uses dynamic dispatch. Pinning and dispatch remain independent axes.
08 · Practical rules
Practical rules I can apply
Normal .await usually needs no explicit Pin
In ordinary async code, the compiler handles the local pinning needed by .await:
async fn handle() -> Result<Response, Error> { let user = load_user(42).await?; build_response(user)}
I normally see explicit pinning when manually polling, storing heterogeneous futures, implementing lower-level future abstractions, or passing futures through type-erased APIs.
Read common compiler errors as design clues
“Cannot be unpinned” usually means an API wants to move or access a future in a way that is not valid after pinning. I first try pin! for a local future or Box::pin for an owned future. I do not blindly add an Unpin implementation.
“Async blocks have different types” means separate generated state machines reached a place that requires one type. I can move the branch inside one outer async block, redesign the generic boundary, use an enum, or intentionally erase the types with Pin<Box<dyn Future<...>>>.
“Borrowed value does not live long enough” or a missing 'static bound is a lifetime/ownership problem. It is not fixed by Pin. For spawned work, moving owned data into the async block is often the right design.
What Pin does not replace
- Ownership still decides who drops the value.
- Lifetimes still decide how long references remain valid.
SendandSyncstill decide thread-safety properties.Futurestill describes asynchronous progress.- The executor still decides when to poll.
My final memory map
async fn ↓ createsconcrete future state machine ↓ may becomeaddress-sensitive while suspended ↓ therefore poll receivesPin<&mut Self> Where is it pinned? How is it called?-------------------- -------------------------stack: pin! static: F / impl Futureheap: Box::pin dynamic: dyn Future Unpin means: moving remains harmless.Pin means: if moving is not harmless, safe code cannot do it after pinning.
The sentence I want to remember is:
Boxanswers where and who owns it.Pinanswers whether the pointee may move.dyn Futureanswers whether its concrete type is erased.
That is enough to decode most async Rust signatures without treating them as one mysterious type.