Join our community of software engineering leaders and aspirational developers. Always
stay in-the-know by getting the most important news and exclusive content delivered
fresh to your inbox to learn more about at-scale software development.
REQUIRED
It seems that you've previously unsubscribed from our newsletter
in the past. Click the button below to open the re-subscribe form
in a new tab. When you're done, simply close that tab and continue
with this form to complete your subscription.
The New Stack does not sell your information or share it with
unaffiliated third parties. By continuing, you agree to our
Terms of Use and
Privacy Policy.
Welcome and thank you for joining The New Stack community!
Please answer a few simple questions to help us deliver the news and resources you are interested in.
REQUIRED
REQUIRED
REQUIRED
REQUIRED
REQUIRED
Great to meet you!
Tell us a bit about your job so we can cover the topics you find most relevant.
REQUIRED
REQUIRED
REQUIRED
REQUIRED
REQUIRED
Welcome!
We’re so glad you’re here. You can expect all the best TNS content to arrive
Monday through Friday to keep you on top of the news and at the top of your game.
What’s next?
Check your inbox for a confirmation email where you can adjust your preferences
and even join additional groups.
Follow TNS on your favorite social media networks.
As modern software demands ever-increasing performance and responsiveness, traditional synchronous programming can become a bottleneck. In server applications, network requests, disk operations and long-running computations often block the main thread, resulting in delays and poor scalability. Rust’s asynchronous programming model addresses this challenge by allowing developers to write nonblocking, highly concurrent code while maintaining memory safety and performance guarantees.
Rust achieves this using Futures and the async/await syntax, enabling tasks to yield control when waiting for external resources and resume efficiently once ready. Combined with powerful runtime libraries like Tokio, Rust can handle thousands of simultaneous operations without the overhead of traditional threads. Let’s explore async programming in Rust, practical use with Tokio and key considerations for building robust, high-performance applications.
Why Async Matters for High-Performance Applications
Synchronous code executes sequentially. Consider a web server that processes HTTP requests:
If each request waits for the database sequentially, the total processing time grows linearly. In high-traffic systems, this leads to high latency and wasted resources.
Async programming solves this by allowing tasks to yield control while waiting for input/output (I/O), letting other tasks progress. Rust accomplishes this without a garbage collector, providing zero-cost abstractions that guarantee both memory safety and predictable performance.
Benefits of async in Rust:
High concurrency: Thousands of tasks can run simultaneously.
Low memory footprint: No need for one OS thread per task.
Safe execution: Rust’s compiler enforces memory and thread safety.
Scalability: Ideal for I/O-bound applications, web servers, microservices and networked systems.
Futures, Async/Await and Executors
Futures
A Future in Rust is an asynchronous computation that produces a value at some point in the future, but not necessarily immediately. Instead of blocking, a Future exposes a poll method that lets the executor check whether it’s ready.
When poll returns `Poll::Pending`, the Future isn’t ready to make progress and yields control back to the executor. Crucially, the Context passed into poll carries a Waker, which the underlying I/O driver or timer clones and stores.
When the external resource becomes ready, such as a socket receiving data or a timer expiring, the driver uses this Waker to notify the executor, prompting it to poll the future again. This Waker-based wake-up mechanism is the foundation of Rust’s nonblocking async runtime, ensuring tasks make progress without ever blocking a thread.
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
struct HelloFuture;
impl Future for HelloFuture {
type Output = String;
fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Self::Output> {
Poll::Ready("Hello, Future!".to_string())
}
}
Here, poll checks if the computation is ready. If not, it yields control to the executor.
Async/Await Syntax
Rust provides the `async` and `await` syntax for more readable asynchronous code:
.await suspends execution until the Future resolves.
This abstraction hides the low-level polling mechanism while maintaining efficiency.
Executors
An executor drives Futures to completion. Common executors include Tokio and `async-std`. Without an executor, async code does not run.
use tokio::time::{sleep, Duration};
#[tokio::main]
async fn main() {
sleep(Duration::from_secs(1)).await;
println!("Executed after 1 second");
}
Using Tokio for Asynchronous Tasks
Tokio is Rust’s most popular async runtime. Features include:
Task scheduling
Timers
Networking (TCP/UDP)
Async file I/O
Concurrent Tasks
use tokio::task;
#[tokio::main]
async fn main() {
let task1 = task::spawn(async { "Task 1 completed" });
let task2 = task::spawn(async { "Task 2 completed" });
let result1 = task1.await.unwrap();
let result2 = task2.await.unwrap();
println!("{}, {}", result1, result2);
}
`task::spawn` allows concurrent execution of tasks without blocking.
Streams and Channels
Streams
A Stream in Rust represents an asynchronous sequence of values, similar to an async iterator. While simple in-memory streams (`tokio_stream::iter`) demonstrate the concept, real systems often deal with unbounded, event-driven streams originating from network activity.
Here is a practical example using `TcpListenerStream`, which converts incoming TCP connections into an asynchronous stream:
use tokio::net::TcpListener;
use tokio_stream::StreamExt;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Bind a TCP listener to a port.
let listener = TcpListener::bind("127.0.0.1:8080").await?;
// Convert incoming connections into a Stream.
let mut incoming = tokio_stream::wrappers::TcpListenerStream::new(listener);
println!("Server listening on 127.0.0.1:8080");
// Each incoming client connection becomes the next item in the stream.
while let Some(stream) = incoming.next().await {
match stream {
Ok(_socket) => {
println!("New client connected!");
}
Err(e) => {
eprintln!("Connection error: {:?}", e);
}
}
}
Ok(())
}
Channels enable safe communication between async tasks:
use tokio::sync::mpsc;
#[tokio::main]
async fn main() {
let (tx, mut rx) = mpsc::channel(32);
tokio::spawn(async move {
tx.send("Hello from task").await.unwrap();
});
while let Some(msg) = rx.recv().await {
println!("{}", msg);
}
}
Async I/O
Async I/O enables nonblocking file, TCP and UDP operations:
use tokio::fs::File;
use tokio::io::{self, AsyncReadExt};
#[tokio::main]
async fn main() -> io::Result<()> {
let mut file = File::open("example.txt").await?;
let mut contents = String::new();
file.read_to_string(&mut contents).await?;
println!("{}", contents);
Ok(())
}
Input Validation and Error Handling
Rust’s error handling integrates naturally with async code using `Result<T, E>`:
async fn fetch_data() -> Result<String, reqwest::Error> {
let response = reqwest::get("https://api.example.com/data").await?;
let body = response.text().await?;
Ok(body)
}
#[tokio::main]
async fn main() {
match fetch_data().await {
Ok(data) => println!("Fetched: {}", data),
Err(err) => eprintln!("Error: {}", err),
}
}
Combine multiple tasks with `tokio::try_join!`:
let (res1, res2) = tokio::try_join!(fetch_data(), fetch_data())?;
Performance Considerations
Minimize allocations: Prioritize stack memory or bytes.
Avoid blocking: Recommend wrapping blocking operations with `spawn_blocking`.
Tune concurrency: Keep in mind that excessive tasks can degrade performance.
Benchmark: Measure latency and throughput using `tokio::time::Instant` or `criterion`.
Real-World Example: High-Performance HTTP Client
use reqwest::Client;
use tokio::time::Instant;
#[tokio::main]
async fn main() {
let client = Client::new();
let start = Instant::now();
let urls = vec![
"https://example.com",
"https://rust-lang.org",
"https://tokio.rs",
];
let handles: Vec<_> = urls
.into_iter()
.map(|url| {
let client = client.clone();
tokio::spawn(async move {
let res = client.get(url).send().await.unwrap();
res.status()
})
})
.collect();
for handle in handles {
println!("Status: {:?}", handle.await.unwrap());
}
println!("Total time: {:?}", start.elapsed());
}
This demonstrates concurrent requests, nonblocking I/O and high throughput.
Advanced Patterns
Task cancellation: `tokio::select!` allows canceling tasks under certain conditions.
Rate limiting: Recommend combining with `tokio::time::sleep` to throttle tasks.
Backpressure handling: Introduce async channels with bounded capacity to prevent flooding.
Parting Thoughts
Rust’s asynchronous programming model is safe, efficient and modern. Rust enables developers to write highly concurrent applications with confidence. By using Futures, `async/await` and the Tokio runtime, developers can handle thousands of concurrent tasks, perform nonblocking I/O and build scalable systems without sacrificing memory safety or performance.
Mastering async Rust is essential for anyone building network services, microservices, real-time systems or high-throughput applications. By combining concurrency patterns, error handling and best practices, Rust provides a powerful foundation for building the next generation of fast, reliable software.
Andela provides the world’s largest private marketplace for global remote tech talent driven by an AI-powered platform to manage the complete contract hiring lifecycle. Andela helps companies scale teams & deliver projects faster via specialized areas: App Engineering, AI, Cloud, Data & Analytics.