InfoQ
Unlike garbage-collected languages where async/await is mere syntactic sugar, Rust required compiler-level async state machines to eliminate the borrow-checker friction and heap allocations that crippled manual futures.
AI-assisted summary of Rust's Journey to Async/Await — original source. Check the original for full context.
Source published:
Key ideas
- Rust originally shipped with green threads and an M:N runtime, but stripped them out entirely in RFC 230 before Rust 1.0 to preserve zero-cost abstractions, predictable performance, and overhead-free C FFI. (19:34)
- Unlike JavaScript promises, Rust's zero-cost futures are lazy and poll-based, avoiding per-step allocations by compiling state machines that an external executor like Tokio drives forward. (29:41)
- While async/await acts primarily as syntactic sugar in garbage-collected languages, it proved critical in Rust to allow the compiler to track borrows across suspension points without forcing heap allocations. (35:36)
- Rust adopted a distinctive postfix `.await` syntax without parentheses to prevent precedence conflicts and allow seamless left-to-right chaining with the postfix `?` error-handling operator. (40:59)
Detailed notes
## Background Published by InfoQ on YouTube on November 11, 2019, this presentation features Steve Klabnik speaking at QCon New York 2019. The talk covers the technical history and design evolution of Rust's concurrency model, detailing the challenges and trade-offs of integrating async/await as a major language feature.
## TL;DR Steve Klabnik traces the multi-year architectural evolution of asynchronous programming in Rust, from early pre-1.0 green threads inherited from its browser-engine origins to their removal in RFC 230 in favor of minimal 1:1 OS threads. He explains how manual zero-cost futures modeled after statically typed functional paradigms clashed with Rust's borrow checker, necessitating language-level async/await to construct stackless state machines without heap allocations. Klabnik details the critical breakthroughs that made stabilization possible, including lazy poll-based execution, pinning (`Pin`) for self-referential memory safety, standardizing `std::future::Future`, and resolving the contentious postfix `.await` syntax.
## Atmosphere & style The presentation carries an engaging, reflective, and pragmatic tone, blending candid engineering war stories with deep technical exposition. Klabnik speaks with self-effacing humor and clarity, openly acknowledging Rust's missteps, ecosystem friction, and painful design pivots without defensiveness. The pacing transitions smoothly from foundational concurrency theory to granular compiler and borrow-checker trade-offs, making the arduous journey toward language stabilization feel both intellectually rigorous and hard-won.
## Bulletin - [19:34](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=1174s) Rust originally shipped with green threads and an M:N runtime, but stripped them out entirely in RFC 230 before Rust 1.0 to preserve zero-cost abstractions, predictable performance, and overhead-free C FFI. - [29:41](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=1781s) Unlike JavaScript promises, Rust's zero-cost futures are lazy and poll-based, avoiding per-step allocations by compiling state machines that an external executor like Tokio drives forward. - [35:36](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=2136s) While async/await acts primarily as syntactic sugar in garbage-collected languages, it proved critical in Rust to allow the compiler to track borrows across suspension points without forcing heap allocations. - [40:59](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=2459s) Rust adopted a distinctive postfix `.await` syntax without parentheses to prevent precedence conflicts and allow seamless left-to-right chaining with the postfix `?` error-handling operator.
## Key takeaways - [01:28](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=88s) Parallelism, concurrency, and asynchrony are orthogonal dimensions of execution, API design, and language semantics. - [04:31](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=271s) Cooperative multitasking relies on voluntary task yields and risks starvation, while preemptive multitasking guarantees fairness at the expense of runtime complexity. - [08:59](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=539s) Green threads reduce initial stack footprints significantly compared to 1:1 OS threads, but incur severe latency and context-switching costs during stack resizing and foreign C calls. - [13:15](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=795s) Because Rust was originally created at Mozilla to build the Firefox client rather than massive web servers, early designs favored green threads without anticipating modern high-density I/O bottlenecks. - [19:34](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=1174s) Rust stripped out its green-thread runtime prior to the 1.0 release via RFC 230 because the runtime overhead conflicted with its core mission as a zero-cost systems language. - [21:18](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=1278s) Shipping Rust 1.0 with only blocking 1:1 native threads forced high-scale server implementations into the same connection-scaling constraints seen in late-1990s web servers. - [24:25](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=1465s) Rust could not replicate Go or Erlang's runtime models because pervasive runtime schedulers introduce FFI thunk overhead that breaks zero-cost C interoperability. - [29:41](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=1781s) Rust futures differ fundamentally from JavaScript promises by being lazy rather than eager, executing only when actively polled by an executor. - [37:28](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=2248s) Manual future combinators generated unreadable types and fought the borrow checker, inadvertently forcing heap allocations and defeating zero-cost performance goals. - [37:28](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=2248s) Language-level async/await allows the Rust compiler to generate state machines that safely borrow data across await points without heap allocation. - [39:35](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=2375s) The introduction of the `Pin` type resolved self-referential memory safety hazards, ensuring generator state machines remain fixed in memory while awaiting. - [39:35](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=2375s) Standardizing `std::future::Future` eliminated the mandatory `Error` associated type, delegating error handling to natural composition with `Result`. - [40:59](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=2459s) The Rust language team selected postfix `.await` over prefix keywords to preserve clean left-to-right code chaining alongside the postfix `?` try operator. - [44:13](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=2653s) Rust's async/await framework seamlessly bridges with browser APIs in WebAssembly via conversion helpers between Rust futures and JavaScript promises. - [48:05](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=2885s) Differing concurrency paradigms—such as Go goroutines, Erlang actors, and Rust futures—represent intentional trade-offs tailored to each language's architectural constraints rather than universal solutions.
## Detailed walkthrough ### [00:03](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=3s) Introduction: Steve Klabnik, Role, and Context
Steve Klabnik opens the talk by noting a recurring pattern in his presentations at QCon: he always seems to be speaking about language features slated to arrive roughly three months in the future. This talk covers the historical path leading up to the stabilization of Rust's `async`/`await` syntax and the final steps remaining in that effort. Klabnik clarifies his position within the Rust organization and set expectations for his perspective: he serves on the Rust core team, leads the documentation team, and works at Cloudflare (though not specifically on Rust projects there).
He underscores that he was not the primary designer behind `async`/`await`, framing his role instead as an observer and chronicler: “I like think of myself as like a amateur historian of rust development I like to keep track of how the language has evolved over time and sort of like deal with these major topics so I was not directly involved in the design I just read all the comments and talked to people over years so I'm kind of giving you my like semi outside perspective on how this went down.” While hundreds of contributors worked on the feature, he highlights withoutboats as the principal driver behind its language design over the preceding years.
[01:28](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=88s) Disentangling Definitions: Parallelism, Concurrency, and Asynchrony
To explain how Rust arrived at its asynchronous model, Klabnik begins by clarifying the terminology of making a computer perform multiple tasks. In conversation, developers routinely conflate three distinct concepts: parallel computing, concurrent computing, and asynchronous programming. He introduces the word *task* as a generic umbrella term for any unit of computation running inside such systems—whether backed by an OS thread, an event loop handler, or another mechanism.
Parallel computing occurs when a system executes multiple tasks truly simultaneously. Because a single CPU core cannot physically perform two instructions at the exact same instant due to the constraints of physical hardware, true parallelism strictly requires multiple CPU cores or multiple CPUs. Concurrency, by contrast, gives the illusion of simultaneous progress on a single core by interleaving execution—doing a small slice of work on one task, switching to another, and returning later. The two concepts can also compose: modern systems routinely run concurrent tasks inside multiple parallel threads across distinct cores. Asynchronous programming exists on a completely separate axis from both. It refers to language features and APIs designed to write non-blocking code that can be scheduled across parallel or concurrent executors, departing from the default line-by-line sequential execution of standard imperative programming.
```mermaid flowchart TD accTitle: Concurrency vs Parallelism Execution Models accDescr: Visual comparison showing parallel tasks running on separate CPUs versus concurrent tasks interleaving on a single core.
subgraph Parallelism ["Parallelism (Multiple Cores/CPUs)"] direction LR P1["Task A (Core 1)"] --> P1_run["Continuous Execution"] P2["Task B (Core 2)"] --> P2_run["Continuous Execution"] end
subgraph Concurrency ["Concurrency (Single Core)"] direction LR C1["Task A (Part 1)"] --> C2["Task B (Part 1)"] C2 --> C3["Task A (Part 2)"] C3 --> C4["Task B (Part 2)"] end ```
- **Task**: A generic catch-all label for any computation executing within a concurrent, parallel, or asynchronous execution context. - **Parallelism**: Hardware-level simultaneous execution requiring two or more physical cores or processors. - **Concurrency**: Logical multi-tasking where computations overlap in time through context switching, even on a single execution core. - **Asynchrony**: A programming model and API design style that breaks synchronous, sequential call-and-wait execution to allow operations to yield while waiting.
[04:31](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=271s) Cooperative vs. Preemptive Multitasking
Klabnik addresses how runtime systems switch between tasks, categorizing multitasking into cooperative and preemptive models. Under cooperative multitasking, each running task explicitly chooses when to surrender execution resources, yielding control back to a scheduler so another task can run. Klabnik recalls first encountering preemptive multitasking around the era of Windows 95 and Mac OS 8. In a preemptive architecture, the underlying operating system or scheduler interrupts computations at arbitrary boundaries without requiring permission from the running code.
The choice between these models represents a classic systems trade-off. Preemption provides safety against uncooperative or malicious code: “This is like a very interesting trade-off because in a system with cooperative multitasking if you have a bad actor they can really mess everything up because what happens if they never decide to give up control of the situation then they just hog all the resources.” While preemption is essential for multi-tenant or untrusted environments, interrupting threads and managing state carries runtime overhead and architectural complexity. Conversely, if a system operates entirely within trusted code, cooperative multitasking can yield lower overhead, provided every task yields responsibly.
- **Cooperative Multitasking**: Tasks yield control voluntarily; an infinite loop or greedy task can starve the entire system. - **Preemptive Multitasking**: The operating system or runtime forcibly interrupts tasks via timer interrupts and context switches, guarding against starvation at the expense of overhead.
[05:45](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=345s) Native Threads (1:1) vs. Green Threads (M:N)
The discussion concludes with the distinction between native threads and green threads, a foundational architectural dilemma that shaped Rust's standard library and I/O model. Native threading—also called 1:1 threading—maps each language task directly to an operating system thread. The operating system handles all thread lifecycle events, priorities, and scheduling across CPU cores. Because the kernel maintains full visibility over every active process and thread on the machine, it can make balanced scheduling decisions across the entire system. Native threads are mature, stable, and conceptually straightforward, but they carry noticeable resource costs. Operating systems must allocate kernel bookkeeping structures and reserve fixed stack sizes (which default to several megabytes on Linux and Windows), putting hard limits on the number of concurrent native threads a program can spawn before exhausting virtual memory.
Green threads—often called M:N or N:M threading—decouple user tasks from kernel threads. The language runtime instantiates its own lightweight user-space tasks and schedules $M$ program tasks onto $N$ underlying OS threads. Many languages have relied on green threads because runtimes can optimize scheduling decisions for application-specific patterns and allocate tiny stacks that grow on demand. However, this decoupling introduces a significant drawback: hiding user-space task states from the operating system kernel can cause application schedulers and the OS scheduler to work at cross-purposes, fighting each other for CPU cache and execution time.
```mermaid flowchart TD accTitle: Native 1 to 1 Threading versus Green M to N Threading accDescr: Diagram illustrating 1-to-1 mapping of application tasks to OS threads versus M-to-N mapping via a language runtime scheduler.
subgraph NativeModel ["Native Threading (1:1)"] direction TB AppTask1["Task 1"] --- OSThread1["OS Thread 1"] AppTask2["Task 2"] --- OSThread2["OS Thread 2"] end
subgraph GreenModel ["Green Threading (M:N)"] direction TB subgraph UserSpace ["Language Runtime"] GTask1["Task 1"] GTask2["Task 2"] GTask3["Task 3"] Scheduler["Runtime Scheduler"] GTask1 --> Scheduler GTask2 --> Scheduler GTask3 --> Scheduler end subgraph KernelSpace ["OS Threads"] OS1["OS Thread A"] OS2["OS Thread B"] end Scheduler --> OS1 Scheduler --> OS2 end ```
- **1:1 (Native) Threading**: - Leverages OS kernel scheduling with system-wide awareness. - Traditional, well-understood API model. - High baseline resource footprint per thread due to OS bookkeeping structures and default multi-megabyte stack allocations. - **M:N (Green) Threading**: - Managed by a language runtime scheduler mapping many user tasks onto fewer OS threads. - Allows highly dense task allocation with minimal initial memory overhead. - Obscures task states from the kernel, creating potential scheduling conflicts between the runtime and the OS.
[08:59](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=539s) The Trade-offs of Green Threads vs. Native Threads
Green threads offer a massive operational advantage when it comes to memory footprint and density. While native operating system threads are heavy, green threads typically initialize with a call stack that is only a tenth to a hundredth of the size of an OS thread's default stack. This dramatically reduced overhead makes it trivial to spawn tens to hundreds of times more threads; in systems architected around green threads, spinning up a million concurrent threads is a completely routine design pattern. Attempting that same scale with native OS threads would rapidly exhaust physical RAM.
However, severing execution from the native operating system introduces harsh disadvantages. Because a green thread starts with a minuscule stack, running out of stack space requires runtime intervention: the runtime must resize the stack dynamically, reallocate memory, and copy the existing stack frames over to the new location. Implementations vary wildly and often introduce unexpected latency spikes. Worse, interoperability with native C code becomes fraught. C ABI expectations demand a standard, continuous native stack. Crossing the boundary between a tiny green thread stack and a native C stack requires context switching and stack swapping that incurs noticeable runtime overhead. As Steve Klabnik observes regarding the architectural friction:
> “And largely the story that I'm going to be telling you today is about how Rust thought that one of these things was the right way to do things and then painfully over years slowly switch to the other one sort of.”
- **Stack size disparity:** Green threads start with 1/10 to 1/100 the stack footprint of an OS thread. - **High-density scaling:** Green thread architectures comfortably support 1,000,000 concurrent threads without RAM exhaustion. - **Stack growth penalties:** Exceeding the initial stack limits forces the runtime to reallocate and copy stack memory. - **C FFI boundary cost:** Calling foreign C functions requires switching to a real native stack, introducing performance overhead.
---
[10:20](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=620s) The C10k Problem and the Evolution of Web Concurrency
To understand why programming languages care about concurrency models, Klabnik revisits the infamous C10k problem—the late-1990s milestone of designing web servers capable of serving 10,000 clients simultaneously. Reading directly from Dan Kegel’s historic 1999 web page, Klabnik highlights the retro computing era when a top-tier server was a 1 GHz machine equipped with 2 gigabytes of RAM and a gigabit Ethernet card for $1,200. He singles out an anecdote from the era that epitomizes the late-90s mindset: in 1999, one of the busiest FTP sites on the internet, `cdrom.com`, successfully handled 10,000 simultaneous clients.
```mermaid flowchart TD accTitle: Evolution of Web Server Concurrency Architecture accDescr: Shows the progression from Apache prefork to worker multi-processing models.
subgraph Prefork ["Early Apache (prefork)"] direction TB L1["Listening Process"] -->|"forks"| P1["Child Process 1 (1 connection)"] L1 -->|"forks"| P2["Child Process 2 (1 connection)"] end
subgraph Worker ["Apache Worker MPM"] direction TB L2["Control Process"] -->|"manages"| CP["Child Process"] CP -->|"thread pool"| T1["OS Thread 1"] CP -->|"thread pool"| T2["OS Thread 2"] CP -->|"thread pool"| T3["OS Thread 3"] end
Prefork -->|"too resource-intensive"| Worker ```
The historical progression of the Apache HTTP Server illustrates the mounting struggle against resource limits. Early Apache utilized a pre-forking architecture: a master control process listened for incoming connections and forked an entire child process for every single incoming client. Dedicating an entire operating system process with its associated memory tables and context-switching overhead to a single socket connection proved far too resource-intensive to sustain exponential web growth. To cope, Apache evolved to the worker MPM (Multi-Processing Module) model, where each child process managed a pool of 1:1 native OS threads that processed connections, achieving significantly higher connection density on the same hardware.
> “Like I just love that sentence because it really positions you in this like late 90s kind of mindset, right? Like FTP site not a thing people use anymore most of the time; CD-ROMs and cdrom.com just like really spells out 1999 for me.”
---
[13:15](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=795s) Early Rust, Firefox, and the Original M:N Green Runtime
Although Rust did not exist in 1999, these historical scaling challenges shaped its early architectural choices. Rust was initially conceived inside Mozilla to improve the Firefox web browser. A web browser is fundamentally an HTTP client, not a massive web server. While client-side networking requires fast, non-blocking I/O to download assets quickly, an individual browser instance never needs to juggle 10,000 or 1,000,000 inbound connections at once. Consequently, massive server-side I/O scalability was not an immediate, first-tier priority for Mozilla's engineering team.
```mermaid flowchart LR accTitle: Early Rust Concurrency Architecture accDescr: Architecture of Rust before 1.0 showing green tasks mapped to OS threads.
subgraph UserTasks ["Green Tasks (Erlang/Go Style)"] T1["Task 1"] T2["Task 2"] T3["Task 3"] T4["Task N..."] end
subgraph Sched ["green module (M:N Scheduler)"] MS["Runtime Work-Stealing Scheduler"] end
subgraph OSThreads ["1:1 Native Threads"] O1["OS Thread 1"] O2["OS Thread 2"] end
UserTasks --> Sched Sched --> OSThreads ```
In pre-1.0 versions of Rust, the language closely resembled Go and Erlang. The standard library included a dedicated `green` module that implemented an M:N green thread scheduler alongside synchronous, non-blocking networking primitives in `std::net`. The underlying design philosophy was simple: developers who wanted concurrent performance would simply spin up thousands of lightweight green threads and write straightforward sequential networking code.
- **Client vs. server focus:** Rust was built for Firefox, where handling millions of concurrent sockets was not the primary operational constraint. - **Erlang and Go influence:** Rust initially leaned heavily toward task-based message-passing and green threads rather than zero-cost system abstraction. - **Standard library green module:** The pre-1.0 distribution shipped an integrated green thread runtime and an M:N task scheduler directly out of the box.
---
[15:15](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=915s) Disentangling I/O Models: Synchronous vs. Asynchronous and Blocking vs. Non-blocking
The early Rust documentation presented what seemed like a paradox: networking I/O was documented as both synchronous and non-blocking. As Klabnik explains, many programmers mistakenly treat "synchronous" as synonymous with "blocking" and "asynchronous" as synonymous with "non-blocking." In reality, these are two independent orthogonal dimensions that define how code is written versus how the execution engine interacts with the operating system.
| Concurrency Dimension | Blocking I/O | Non-blocking I/O | |:--- |:--- |:--- | | **Synchronous Code** | **Classic Blocking:** The source code appears sequential, and thread execution halts completely on I/O (e.g., classic MRI Ruby, legacy interpreters). | **Synchronous Non-blocking:** The code is authored sequentially, but the underlying runtime intercepts I/O, yields the green thread, and unblocks transparently (e.g., Go, modern Ruby, early Rust). | | **Asynchronous Code** | **Nonsensical:** Code is authored with asynchronous callbacks/promises, yet intentionally halts the execution thread. No mainstream languages use this pattern. | **Asynchronous Non-blocking:** The code explicitly models asynchronous boundaries (callbacks, promises, futures), and runtime I/O does not block the thread (e.g., Node.js). |
Synchronous blocking I/O is intuitive to write, but its execution performance is low because threads spend significant time sitting idle. Synchronous non-blocking I/O (as used in Go, modern Ruby, and early Rust) offers an appealing compromise: developers write simple, linear code without callback hell or promise wiring, while the runtime engine transparently turns socket reads and writes into non-blocking operations, parking and resuming lightweight tasks behind the scenes. This model made it easy for scripting languages like Ruby to upgrade their internal runtimes for better I/O performance without requiring existing user code to change.
> “When you write Go code, you're not writing in a way that looks async, you're writing in a way that looks like it goes through step by step by step, but inside in the runtime it actually transparently does non-blocking I/O and it will pause your program there, whereas like if you look at Node.js, it's asynchronous and non-blocking.”
---
[17:45](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=1065s) The Conflict with Rust's Identity as a Systems Language
Early Rust selected the synchronous non-blocking model paired with green threads because it gave users readable sequential syntax with non-blocking network performance. However, this design soon ran headfirst into Rust’s core ambitions. Early branding promoted Rust as a true systems programming language designed to run “blazingly fast, prevent almost all crashes, and eliminate data races.”
A systems language is expected to interface directly with hardware, produce lean binaries without mandatory runtimes, and embed seamlessly into existing C codebases. A mandatory M:N green thread runtime fundamentally clashed with that mission. Native operating system threads are provided directly by the OS kernel, whereas green threads require an omnipresent language runtime with scheduler threads, stack-checking logic, and complex foreign function interface (FFI) thunks. As Rust matured toward its 1.0 milestone, carrying a heavy, mandatory runtime began to undermine its promise of zero-cost abstractions.
> “See this is what the Rust website used to look like a long time ago, and it describes Rust as a systems programming language that runs blazingly fast, prevents all crashes, and eliminates data races. But when I told you before about like green threads versus system threads, like green threads are provided by your runtime and native threads are provided by your OS...”
[18:07](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=1087s) The Dual-Runtime Compromise and the Fork Threats
In the early days of Rust's development, the core team confronted an existential identity crisis: what did it actually mean to build a systems programming language if developers were denied direct access to operating system threads? Blocking access to native OS threads seemed completely incoherent for a language with low-level ambitions. This friction sparked one of the earliest and most bitter battles in Rust's design history, to the point where community members actively threatened to fork the language.
```mermaid flowchart TD accTitle: Rust 1:1 vs M:N Threading Dilemma accDescr: Diagram showing how Rust attempted to support both native OS threads and green threads under a shared runtime interface.
subgraph App["Application Code"] Code["User Program"] end
subgraph Runtime["Runtime Abstraction Layer"] Interface["Shared Runtime Interface"] end
subgraph Engines["Execution Models"] Native["Native 1:1 OS Threads<br/>(Heavyweight, direct OS API)"] Green["Green M:N Threads<br/>(Bloated by bookkeeping)"] end
Code --> Interface Interface --> Native Interface --> Green ```
To quell the uprising, the team tried a diplomatic middle ground: they offered both. In subsequent releases, the standard library shipped two distinct packages—`native` for 1:1 operating system threading, and `green` for M:N user-space green threading. The vision was that programmers could write against a unified, swappable runtime abstraction interface and choose whichever model suited their workload at deployment time. On paper, having all the options appeared ideal, but in practice, trying to unify both models under a single interface collapsed under its own weight.
Despite the compromise, developer hostility persisted. Steve Klabnik recalls the drama with humor: “Someone did and then like five people favorite on github and nobody used the fork anyway but like it was a serious problem and like you know we weren't like happy with this situation and part of the reason why is so this is like an example of an RFC where we eventually decided to remove the runtime.”
[19:34](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=1174s) RFC 230 and Shipping Minimal OS Threads in Rust 1.0
The technical reason the dual-runtime approach failed was the cost of abstraction. To build an interface flexible enough to bridge 1:1 native threads and M:N green threads, so much bookkeeping and overhead had to be added to green threads that they ceased being lightweight. Instead of being fast and nimble, green threads degraded into a clunky, inferior imitation of native threads, with worse performance and zero tangible benefit. Permitting both models produced an unmaintainable, overly complicated mess.
With the 1.0 release rapidly approaching, the team knew they would soon freeze the language and standard library interfaces permanently. Continuing with a flawed, half-baked concurrency model was out of the question. Through Rust's open design process, RFC 230 was drafted and accepted, officially stripping green threads and the entire runtime subsystem out of the language. Because Rust was fundamentally a systems language, OS threads were already provided by the underlying platform and worked reliably without runtime baggage.
When Rust 1.0 launched in May 2015, it shipped with a very minimal, blocking, 1:1 OS threading model and zero runtime magic. This austere design choice enabled the team to stabilize the language and hit the 1.0 release milestone. However, as Klabnik points out, while that resolved the initial controversy from 2011–2012, it was only the midpoint of an eight-year arc spanning all the way to 2019.
- Rust RFC 230 formally eradicated green threads and the runtime abstraction layer from the standard library. - Rust 1.0 shipped in May 2015 with exclusively native, synchronous, blocking OS threads. - The design decision favored zero-cost predictability and direct systems-level integration over bundled language runtimes.
[21:18](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=1278s) The Network Service Dilemma: CPU-Bound vs. I/O-Bound
Rust 1.0 was a runaway success, but that success brought new pressures. Had Rust faded into obscurity, its minimal blocking thread API would have remained perfectly adequate. Instead, developers fell in love with the language and immediately wanted to expand its scope beyond client applications and Firefox components: they wanted to write high-concurrency network services in Rust. When they attempted to build servers using standard blocking OS threads, they ran headfirst into the architectural wall of late-1990s Apache.
Steve Klabnik highlights the tension between Rust's branding and its real-world server performance: “Russ is supposed to be a high-performance programming language but like the IO IO model feels like it's out of 1999 like old-school Apache like spin up all these big heavyweight threads and it definitely wasn't performing as well as people want it to be even though rust is like supposed to be a super high performance language and like how could we claim that like we're as fast as C well just not literally being as fast as C because IO is like a problem.”
The root cause was the classic architectural mismatch between CPU-bound and I/O-bound workloads:
| Workload Type | Defining Characteristic | Performance Bottleneck | Ideal Concurrency Model | |:--- |:--- |:--- |:--- | | **CPU-Bound** | Processor actively executing calculations and crunching numbers | Speed and core count of the local CPU | Multi-threaded parallelism across hardware cores | | **I/O-Bound** | Waiting on network roundtrips, disk responses, or remote endpoints | Latency of external systems and network sockets | Asynchronous event-driven I/O or green routines |
In an I/O-bound service, a machine spends the vast majority of its time doing nothing while waiting on remote peers. Because “the fastest thing to do is nothing at all,” an I/O system scales by letting connections idle without dedicating a costly operating system thread to each one. Tying up full OS threads—along with their associated memory stacks and scheduling overhead—just to wait on idle network sockets crippled Rust's throughput.
[24:25](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=1465s) Why Rust Could Not Adopt Go or Erlang's Green-Thread Model
Languages like Erlang and Go solved the I/O-bound scaling problem by implementing asynchronous I/O under the hood while exposing a clean, synchronous-looking programming model backed by lightweight green threads (or goroutines). When an I/O call is initiated, the language runtime suspends the goroutine and yields the underlying OS thread to other work. While this is a battle-tested and effective pattern, it inherently relies on a built-in runtime scheduler.
```mermaid flowchart LR accTitle: C FFI Overhead in Green Thread Runtimes accDescr: Flow diagram showing how entering C code forces runtime stack switching or thread-parking in Go/Erlang vs zero-cost calls in Rust.
subgraph GreenRuntimes["Go / Erlang Runtime Model"] direction TB G1["Lightweight Routine"] --> Sw["Context / Stack Switch Overhead"] Sw --> CF1["Foreign C / OS Call"] end
subgraph RustNative["Rust Zero-Cost Model"] direction TB R1["Native Rust Frame"] -->|"Zero Overhead"| CF2["Foreign C / OS Call"] end ```
For Rust, the fatal flaw of green threads remained foreign function interface (FFI) overhead. Whenever an M:N green thread calls into foreign C code or native OS routines, the runtime must perform expensive stack-switching gymnastics or pin execution to a dedicated OS thread. That trade-off is acceptable for Go (which prefers rewriting everything in Go) or Erlang (which is rarely used for raw number crunching). But for Rust, zero-cost C interoperability was a non-negotiable founding principle.
As Klabnik explains: “And our constraints are that we had no overhead and calling into C we designed the language itself around this kind of thing so if we had to do a FRA oh that would be terrible and so this is also like part of why we got rid of the green green threading API that existed a long time ago too this is like a second reason that didn't work but also sort of why we couldn't really return to it.”
[25:57](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=1557s) The Alternative Path: Nginx, Event Loops, and Ryan Dahl's Node.js
Since Rust could not return to green threads without abandoning its core zero-overhead guarantee, it had to look elsewhere for scalable I/O. The alternative architectural blueprint came from high-performance web servers like Nginx. Rather than spinning up an individual thread per connection, Nginx used evented asynchronous I/O managed by an event loop.
In an evented model, the server operates on an event loop—often driven by a single thread. The program registers interest in specific I/O events (such as a socket becoming readable or writable) alongside corresponding handlers, and the loop fires those handlers only when the events actually trigger. Because idle connections remain completely passive, an event loop can juggle tens of thousands of concurrent network connections without allocating heavy operating system thread stacks.
This event-loop design vaulted into mainstream software engineering consciousness in 2009 when Ryan Dahl gave his landmark presentation introducing Node.js at the first JSConf EU. Showing a grainy screenshot taken from the original hand-cam recording of that talk, Klabnik illustrates how Node.js popularized non-blocking event-driven I/O for network programming, pointing the way toward how modern languages could tackle massive concurrency without thread-per-connection bloat.
[26:57](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=1617s) Node.js, Non-Blocking I/O, and Callback Hell
```mermaid flowchart LR accTitle: Evolution from Callbacks to Chained Promises accDescr: Shows the structural shift from nested asynchronous callback chains that drift rightward to linear method chaining with promises.
subgraph CB["Callback Pattern"] direction TB C1["readFile(...)"] --> C2["Callback 1"] C2 --> C3["Nested Callback 2"] C3 --> C4["Nested Callback 3 ('Rightward Drift')"] end
subgraph PR["Promise Pattern"] direction TB P1["createPromise()"] --> P2[".then(...)"] P2 --> P3[".then(...)"] P3 --> P4["Vertical Method Chaining"] end
CB -->|"Refactored into"| PR ```
Steve Klabnik opens by apologizing for a degraded, low-resolution screenshot taken from a video on YouTube, joking that when an AV setup is mediocre, you put it on YouTube, screenshot it, and paste it into a slide deck, “you get this.” The slide shows the fundamental definition of Node.js: server-side JavaScript built on Google's V8 engine with evented, non-blocking I/O. Ryan Dahl cared deeply about handling I/O efficiently, and making that possible was the very reason Node existed.
However, event-driven architectures demand non-blocking APIs. In Node, a blocking call like `fs.readFileSync` stops execution until the file is loaded into memory. To run non-blocking operations against an event loop, programs must register event listeners using callbacks. While passing a single callback closure to `fs.readFile` looks completely innocuous in simple snippets, real-world software quickly descends into "callback hell." Referencing the site `callbackhell.com` and an iconic video game meme, Klabnik notes:
> “Somebody made Ryu giving a Hadouken like to like click this thing to the pushing everything over, I don't know if anyone plays video games here, but the point is is that like when you start writing this callback stuff, stuff start drifting right and you can't really like do a whole lot about it.”
To escape this awkward horizontal drift, the JavaScript community adopted Promises. By definition, a Promise represents an eventual value from an asynchronous computation that begins in a `pending` state and resolves to either `fulfilled` or `rejected`. Rather than nesting callbacks deeper and deeper, developers can chain `.then()` handlers linearly:
```javascript myPromise.then(/* step 1 */).then(/* step 2 */); ```
While chaining replaces a deep rightward drift with a vertical chain of closures, repeatedly chaining endless `.then()` invocations remains clumsy and awkward to maintain.
[29:41](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=1781s) Scala’s Futures and Rust's 0.1 Zero-Cost Implementation
```mermaid flowchart TD accTitle: Rust Future Execution Model accDescr: Contrast between eager JavaScript promises and Rust's lazy, compiler-optimized zero-cost futures executed via an executor.
subgraph JS["JavaScript Promises (Eager)"] J1["Create Promise"] --> J2["Runtime Starts Execution Immediately"] J2 --> J3["Heap allocation per chained step"] end
subgraph RS["Rust Futures 0.1 (Lazy & Zero-Cost)"] R1["Chain Futures (.and_then)"] --> R2["Compile to Single Exact State Machine"] R2 --> R3["Pass task to Executor (e.g., Tokio)"] R3 --> R4["Executor calls poll() until ready"] end ```
Simultaneously, Twitter faced severe scalability bottlenecks after building their initial infrastructure on Ruby on Rails. To resolve their I/O constraints, Twitter migrated to the JVM, embraced Scala, and published a landmark paper titled *Your Server as a Function*. The paper detailed their high-performance network services using an abstraction known as a "Future." Observing that Scala was statically typed just like Rust, the Rust team realized that futures fit their language model far better than nested callbacks or dynamic promise designs.
In August 2016, Rust announced its zero-cost futures library. In `futures 0.1`, the core abstraction was the `Future` trait—effectively an interface requiring two associated types (`Item` and `Error`) and a primary polling method:
```rust trait Future { type Item; type Error;
fn poll(&mut self) -> Poll<Self::Item, Self::Error>; } ```
Combinators like `.and_then()` allowed chaining asynchronous actions sequentially, mirroring JavaScript promises under a slightly different name.
The fundamental architectural divergence between JavaScript promises and Rust futures boils down to runtime dependencies and resource allocation:
- **JavaScript Promises are Eager:** Built directly into the language runtime, a Promise begins executing the instant it is constructed. Because tasks execute before the full chain is necessarily assembled, every chained link requires its own memory allocation. - **Rust Futures are Lazy:** Rust cannot ship a mandatory heavy runtime because it targets low-level systems workloads alongside C. A Rust future does nothing when defined; it represents an idle computation that only progresses when explicitly driven by an executor via the `poll` method. - **Single Static Allocation:** Because the compiler sees the entire chain of futures upfront, it calculates the exact memory size needed for the task at compile time. It compiles down to an optimal hand-written state machine requiring only a single allocation (or zero dynamic allocations in restricted environments) without needing resizable green-thread stacks.
An example application combining `futures 0.1` and Tokio demonstrated this paradigm: creating a TCP listener with `listener.incoming()`, chaining error handlers and combinators onto incoming sockets, and then passing that aggregate task graph into the runtime runner via `tokio::run(server)`.
- Twitter's paper *Your Server as a Function* outlined their transition from Ruby on Rails to Scala on the JVM. - Rust's zero-cost futures were formally unveiled in August 2016. - Tokio emerged as the primary asynchronous runtime executor to drive these poll-based state machines.
[34:36](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=2076s) The Context Problem and the Futures 0.2 Transition
As developers built substantial real-world applications atop `futures 0.1`, notable architectural flaws surfaced. A major design issue was its hidden reliance on thread-local storage (TLS) for internal task bookkeeping and waking notifications. This TLS requirement meant `futures 0.1` could not function in environments lacking OS threads, such as embedded hardware and bare-metal targets.
To address this, `futures 0.2` altered the polling contract by making task state explicit through an added parameter:
```rust fn poll(&mut self, cx: &mut task::Context) -> Poll<Self::Item, Self::Error>; ```
While passing explicit context resolved the bare-metal roadblock, the release created serious ecosystem friction. Recognizing that `0.2` was merely an incremental snapshot on the way to a more comprehensive overhaul, the team signaled that `0.3` would quickly follow. When ecosystem maintainers asked whether they should upgrade their crates from `0.1` to `0.2` or wait for `0.3`, Aaron Turon published a blog post advising people not to migrate the whole ecosystem over to `0.2`.
This recommendation placed developers in an impossible paradox: libraries needed to update for users to actually test the new design, but updating to an intermediate version that would be deprecated within months seemed futile:
> “Aaron, who wrote this blog post, said you shouldn't—you shouldn't move everything over. Two is a good enough snapshot, but we're gonna have three coming out in a couple of months or less. And then people are like, that seems a little bad, cuz like you need to try it out in order to use it, but your libraries update, how are you supposed to try it out? And then if it's gonna change again, why would anyone try it out in the first place? This person was smart.”
[35:36](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=2136s) Async/Await: From Convenience in Managed Languages to Necessity in Rust
Steve Klabnik looks back to the optimistic timeline Rust set a year prior—promising async/await in just a "couple months"—and contrasts how asynchronous execution developed across languages. Returning to JavaScript as an illustration, code evolved from nested callback hell through promise chains that mitigated rightward drift, and finally to `async`/`await`. While C# actually originated the `async`/`await` concept, JavaScript adopted it wholesale to eliminate the pain of managing millions of promises manually. In these garbage-collected languages, the feature functions essentially as an ergonomic convenience: it takes code that performs non-blocking work and lets developers write it sequentially so that it reads like traditional blocking logic.
In Rust, however, the role of `async`/`await` turned out to be far more profound. Managed languages possess runtimes and garbage collectors that easily keep track of intermediate allocations scattered across callback closures and promise chains. When Rust attempted to use manual future chaining in the `futures 0.2` era, the absence of a garbage collector turned that pattern into an agonizing battle with lifetimes. Developers had to manually track ownership across separate future stages and chains, creating friction that Rust's type system could not smoothly navigate. As Steve explains: “And here's the thing that's interesting: we didn't actually realize until we played with `futures 0.2` even more, but async/await is actually more significant to Rust than in other languages.”
- C# originally invented the `async`/`await` pattern, which JavaScript later adopted to simplify promise chaining. - In managed runtimes, `async`/`await` is primarily syntactic sugar over promises or tasks because the garbage collector handles intermediate memory. - In Rust, manual futures forced developers to grapple directly with ownership, lifetime tracking, and memory allocations across asynchronous boundaries.
[37:28](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=2248s) The Compiler vs. Manual Futures: Lifetimes and Borrow Checking
To demonstrate why manual futures fell short, Steve shows a standard synchronous `read` method that takes `&mut self` and a buffer, filling the cursor in 1,024-byte chunks and returning the count of bytes read or an error. When attempting to translate that clean, straightforward routine into a pure futures-based implementation, the signature and associated plumbing ballooned so dramatically that the code could not physically fit onto a presentation slide. The root cause lay in the separation between creation and execution: a future is constructed up front, but handed off to an executor to run later. The Rust borrow checker was forced to apply the most restrictive rules imaginable because it could not track how memory was being passed and borrowed across those disjoint temporal steps.
This friction created a painful paradox reminiscent of Rust's early struggle with green threads versus native threads. To satisfy the compiler's strict borrow checker without compiler-level support for suspension points, developers had to resort to extra heap allocations, reference counting, and runtime checking. The zero-cost, hyper-efficient theoretical design degraded into something both slow and brutally painful to write. The arrival of language-level `async`/`await` solved this dilemma by making the compiler and borrow checker aware of the state machine's internal lifecycle. By wrapping the logic in an `async` block and using `await`, the code shrank right back down to looking nearly identical to the synchronous version while preserving memory safety without runtime overhead. As Steve put it: “It turned out the code you had to write to use the hyper-efficient thing was very inefficient, so we're now back to a place where like this is not fast and it's hard to write, so life is terrible—but async/await to the rescue.”
```mermaid flowchart TD accTitle: Synchronous versus Manual Futures versus Async-Await accDescr: Flow diagram contrasting the ergonomic synchronous approach, the broken ergonomics of manual futures, and the resolution provided by async-await.
subgraph SyncFlow["Synchronous Approach"] S1["Clean API signature"] --> S2["Simple sequential loop"] S2 --> S3["Direct stack borrows"] end
subgraph ManualFutures["Manual Futures Chaining"] F1["Massive type signatures"] --> F2["Future constructed first, executed later"] F2 --> F3["Borrow checker rejects lifetime overlap"] F3 --> F4["Forced allocations and runtime checks"] end
subgraph AsyncAwaitFlow["Language-Level Async/Await"] A1["Sequential code in async block"] --> A2["Compiler compiles state machine"] A2 --> A3["Borrow checker understands suspension points"] end
ManualFutures -->|"Solved by"| AsyncAwaitFlow ```
- Synchronous I/O in Rust uses clean signatures like `read(&mut self, buf: &mut [u8]) -> Result<usize>`. - Manual future chaining failed because the borrow checker could not reconcile borrows held across points where futures are instantiated versus when executors poll them. - To satisfy lifetime errors in pure manual futures, programmers were forced to introduce extra box allocations and runtime checks, undermining Rust's zero-cost guarantees. - Integrated `async`/`await` syntax teaches the compiler how to track borrows across yield points without heap-allocating every frame.
[39:35](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=2375s) Redesigning the Future Trait: Infallibility and Pinning
Even after addressing memory ergonomics with `async`/`await`, the `Future` trait itself required a major architectural overhaul before it could graduate into the standard library as `std::future::Future`. In early revisions, futures were conceived strictly around I/O, meaning the trait definition baked an associated `Error` type directly into its interface. However, as asynchronous patterns permeated real-world software, developers found that futures infected entire codebases, including many operations that could never fail. Forcing infallible tasks to define dummy error types produced immense boilerplate for simple logic. The updated trait simplified this by dropping the dedicated error type in favor of a single `Output` type; if an operation can fail, it simply returns a standard `Result<T, E>` as its output.
The second major change to the trait signature was the introduction of `Pin`. Steve intentionally avoids diving deep into `Pin`, joking that it deserves its own dedicated 45-minute presentation. In short, `Pin` ensures that self-referential data structures generated by the compiler's `async`/`await` transformation cannot be moved in memory, which is what fundamentally allows the borrow checker to safely manage references across suspension points. Most end users will never need to interact with it directly, as it primarily concerns low-level library and runtime authors. Describing the decision to keep the core trait minimal, Steve observed: “And then finally, if you need a future that has an error, you just compose our regular old `Result` type as the `Output` type, and so futures that don't have errors are fine, futures that do have errors still work, life is good, right?”
- The original `Future` trait assumed all futures performed I/O and thus mandated an associated `Error` type. - Baking errors into the trait caused boilerplate because many internal asynchronous computations are infallible. - The standardized `std::future::Future` trait contains only a single `type Output`, allowing fallible futures to yield `Result<T, E>` naturally. - `Pin` was added to `poll(self: Pin<&mut Self>, cx: &mut Context<'_>)` to prevent memory moves of self-referential generator frames, teaching the borrow checker how to validate asynchronous borrows.
[40:59](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=2459s) The Great Syntax War: Postfix `.await` vs. Prefix `await`
The question of which syntax to adopt for awaiting a future ignited one of the fiercest bikeshedding debates in Rust's history. Other languages like JavaScript and C# employ prefix notation, writing `await future`. However, Rust features the postfix try operator (`?`) for concise error propagation. Because asynchronous operations routinely produce errors, developers need to both await a future and bubble up its potential failure. Under a prefix `await`, writing `await foo?` created an ambiguous precedence problem: did it mean `(await foo)?` or `await (foo?)`? Almost every practical use case required resolving the future first before checking the `Result`, meaning developers would either have to wrap expressions in noisy parentheses like `(await foo)?` or the language would have to introduce confusing operator-precedence exceptions specifically for `await`.
```mermaid flowchart LR accTitle: Precedence Ambiguity in Await Syntax accDescr: Diagram contrasting how prefix await interacts awkwardly with the question mark operator versus the linear evaluation of postfix dot await.
subgraph PrefixIssue["Prefix: await expr?"] P1["await foo?"] --> P2{"Ambiguity"} P2 -->|"Intuitive parse"| P3["await (foo?)"] P2 -->|"Desired logic"| P4["(await foo)?"] end
subgraph PostfixSolution["Postfix: expr.await?"] S1["foo.await?"] --> S2["1. foo.await resolves future"] S2 --> S3["2.? unwraps Result"] end ```
The community erupted into exhaustive debates across Discourse, Reddit, and issue trackers, proposing everything from special inverted question marks to custom keywords. A screenshot of one maintainer's email client showed dozens of threads overflowing with hundreds of unread messages. Every conceivable permutation had vocal detractors, and no option achieved consensus. Eventually, the language team selected postfix `.await`—a keyword-based dot syntax with no parentheses (`future.await` or `future.await?`). While some community members viscerally disliked the departure from mainstream syntax, it preserved clean left-to-right method chaining and seamless pairing with `?`. Steve noted that while it was not his personal favorite either, moving the language forward was what mattered most: “And so at some point we decided like, hey, we can't let this incredibly important feature to Rust just be stuck down in the syntax decisions, and so we picked one.”
- Prefix syntax (`await expr?`) suffered from operator precedence conflict with Rust's postfix error operator `?`. - Chaining multiple prefix calls in JavaScript and C# often requires nesting or outer parentheses, which clashed with Rust's chained method-call conventions. - Community discussions exploded into massive arguments, logging hundreds of replies across Discourse threads, Reddit posts, and email chains. - The language team finalized postfix `.await` without parentheses, treating `await` as a reserved postfix keyword that supports left-to-right evaluation like `fetch().await?`. - Even though opinions were sharply polarized initially, developers typically adapted to postfix `.await` within fifteen minutes of practical usage.
[44:13](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=2653s) Ergonomics: Runtime Macros and Async/Await in WebAssembly
Beyond the core syntax debates, the Rust ecosystem has continued developing ergonomics around async/await to make code feel natural and approachable. Steve Klabnik showcases an example using a crate named `runtime`. In a UDP echo example, the main function is decorated with a `#[runtime::main]` attribute macro. While Rust requires submitting futures to an executor, this macro rewrites the underlying code automatically, making it resemble Node.js where executor submission is handled behind the scenes. Without explicit syntax highlighting on the slide it appears somewhat dense, but inside an editor, it provides a clean, familiar structure.
```mermaid flowchart LR accTitle: JS Promise and Rust Future conversion in WebAssembly accDescr: Diagram tracing a database call returning a JS Promise into a Rust Future and back into a JS Promise. subgraph Browser ["JavaScript Runtime"] A["JS Database Call"] -->|"returns"| B["JS Promise"] F["JS Promise Output"] end subgraph RustWasm ["Rust WebAssembly Module"] C["JsFuture::from"] -->|"converts"| D["Rust Future (.await)"] D -->|"future_to_promise"| E["Converted Future"] end B --> C E --> F ```
Klabnik then highlights a wilder capability: using async/await directly inside WebAssembly (WASM). He walks through a real production example running on his own website that fetches data from a database. Because the underlying database access utilizes a JavaScript API, calling `data.get()` returns a JavaScript promise. In Rust, this is wrapped using `JsFuture::from()`, yielding a specialized Rust future that bridges the JavaScript promise so it can be awaited with `.await`. Once the data is processed, `future_to_promise` converts the Rust future back into a native JavaScript promise that is exported back to the calling environment. As Klabnik humorously describes the setup: “So yes this code and wasm is a JavaScript promise wrapped up in a rasp rust future wrapped up in a JavaScript promise and then returned through rusts back into webassembly back into JavaScript and this works it's totally fine I mean depending your definition of fine I guess but like it is a thing that if you go to my website right now this code will execute and it's a thing.”
- The `#[runtime::main]` macro hides explicit executor bootstrapping and task spawning, mirroring the effortless feel of Node.js runtimes. - `JsFuture::from` allows Rust code compiling to WebAssembly to await native browser or JavaScript promises directly. - `future_to_promise` serializes completed Rust async results back into JavaScript promises across the WASM boundary.
[46:24](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=2784s) Stabilization Timeline and TechEmpower Benchmarks
Reflecting on the journey across various concurrency models, Klabnik summarizes how the team examined nearly every design space in existence before arriving at zero-cost futures. While his original conference slide predicted stabilization in Rust 1.37 or 1.38, he shares breaking news from that very morning: Withoutboats tweeted that async/await is officially being stabilized, guaranteeing its arrival in Rust 1.38, set for release around August 15. Nearly a decade after work began around 2010–2011, Rust finally established its comprehensive, zero-cost I/O model.
To illustrate the performance payoff of these design efforts, Klabnik references a recent run from the TechEmpower benchmarks. Actix, a Rust web framework, dominates the charts: the nearest competing framework from another language reaches only 62% of Actix's throughput. By refusing to compromise on runtime overhead or memory allocations, the community achieved industry-leading speeds alongside ergonomic syntax.
[48:05](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=2885s) Key Takeaways: Time, Architecture, and Language Constraints
In his closing moments, Klabnik distills two fundamental lessons from the decade-long evolution of Rust's async story. First, building an efficient, production-grade I/O system cannot be rushed—it takes years of iteration, profiling, and redesign. Second, programming languages operate under vastly different design constraints, making direct comparisons between paradigms like Go's goroutines, Erlang's actor model, and Rust's poll-based futures unhelpful without context.
Klabnik emphasizes that architectural choices must respect a language's specific priorities: “Different languages have different constraints this means that you can't really argue that like go or Erlang style is better or worse than Russ style they have different pros and cons in all directions and so like just because you like a thing in one language and you move to a different language does not mean that the other language did a different way for a wrong reason.” Rust could not simply copy existing solutions because its strict non-negotiable guarantees—zero runtime overhead, no mandatory garbage collector, and cross-platform systems integration—required an entirely original path.
- Building a world-class I/O system is an iterative engineering process spanning many years. - Concurrency paradigms from Erlang or Go are neither inherently superior nor inferior; they balance different engineering trade-offs. - Rust's constraints required developing custom zero-cost abstractions rather than borrowing existing runtime models.
## Notable quotes > [35:36](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=2136s) async/await is actually more significant to Rust than in other languages. — Steve Klabnik
## Mentioned Aaron Turon, Actix, Apache HTTP Server, C#, cdrom.com, Cloudflare, Dan Kegel, Erlang, Firefox, Go, Google V8, JSConf EU 2009, Mozilla, Nginx, Node.js, QCon New York 2019, RFC 230, Rust, Ryan Dahl, Ryu, Scala, Steve Klabnik, TechEmpower, Tokio, Twitter, WebAssembly, Withoutboats, Your Server as a Function
## Video description Video with transcript included: http://bit.ly/2XcjHbj
Steve Klabnik gives an overview of Rust’s history, diving into the technical details of how the design has changed, and talks about the difficulties of adding a major new feature to a programming language.
This presentation was recorded at QCon New York 2019: http://bit.ly/2KFk7SO
The next QCon is QCon London 2020 – March 2-4, 2020: http://bit.ly/2VfRldq
For more awesome presentations on innovator and early adopter topics check InfoQ’s selection of talks from conferences worldwide https://bit.ly/2tm9loz
#Rust #Async #ProgrammingLanguage
[Source](https://www.youtube.com/watch?v=lJ3NC-R3gSI)