Why Everyone Ends Up Using Tokio

I've been using Tokio since the 0.1 days when async Rust was still experimental as hell. Back then, you had to pick between Tokio and async-std, and honestly, most of us went with Tokio because it had more ecosystem support. Now async-std is officially dead as of March 2025, so that choice got easier.

The core problem Tokio solves is that traditional threading is expensive. Java threads eat about a meg of memory each, so you hit limits around 30,000 threads on decent hardware. With Tokio, tasks are cheap - maybe 2KB each - so you can spawn hundreds of thousands without your server keeling over.

How Tokio Actually Works

The work-stealing scheduler sounds fancy but really just means your CPU cores stay busy. Each core gets its own task queue, and when one runs out of work, it steals from the others. Pretty clever, and it mostly works well in practice.

Tokio Work Stealing Scheduler

Under the hood, Tokio uses epoll on Linux, kqueue on macOS, and IOCP on Windows to efficiently wait for I/O events. This means one thread can monitor thousands of network connections without blocking. The reactor pattern isn't new - nginx has been doing this forever - but Tokio makes it accessible in Rust.

Real Performance in Production

Discord's blog post about switching from Go to Rust is worth reading for actual performance numbers, not marketing fluff. They were hitting Go's garbage collector limits with their message volume, and Tokio helped them handle millions of concurrent WebSocket connections.

I've personally seen Tokio handle 100K+ concurrent connections on a single server with 16GB RAM. The limiting factors are usually file descriptors (bump your ulimits) and network bandwidth, not Tokio's scheduler.

The Learning Curve Is Real

Here's what the docs don't tell you: async Rust has a steep learning curve. If you're coming from blocking I/O, you'll spend hours debugging why your code hangs. Async stack traces are garbage, and error messages often don't make sense until you understand the borrow checker's relationship with futures.

The `Send + Sync` requirements for spawned tasks will bite you. You'll try to share a `Rc>` across tasks and get compiler errors that look like hieroglyphics. Use `Arc>` or channels instead.

Ecosystem Lock-in (But It's Good)

Once you pick Tokio, you're pretty much locked into its ecosystem. Axum, Hyper, Tonic, Tower - they all assume Tokio. This isn't necessarily bad since the ecosystem is mature, but switching async runtimes later is a pain in the ass.

The good news is that Tokio's ecosystem actually works together. Unlike JavaScript where every HTTP client has different interfaces, Tokio crates generally play nice with each other.

Tokio vs The Alternatives That Are Left

Feature

Tokio

async-std

smol

Status (2025)

Still going strong

Dead as of March 2025

Works fine

Ecosystem

Everything uses it

Used to compete, now dead

Small but growing

Memory per task

~2KB (depends on your code)

Dead runtime, who cares

~2KB

Threads

Multi-threaded by default

Was multi-threaded

Single-threaded unless you tell it otherwise

I/O Backend

mio (solid, battle-tested)

Dead

polling (works across platforms)

Learning curve

Steep as hell

Was easier

Actually simple

Production ready?

Used everywhere

Was deprecated

Good for simpler stuff

WASM support

Barely works

N/A (dead)

Actually decent

Documentation

Comprehensive but overwhelming

Was good, now archived

Minimal but clear

Getting Started (And What Will Go Wrong)

Adding Tokio to Your Project

Add tokio to your Cargo.toml like every other dependency. Don't overthink it:

[dependencies]
tokio = { version = "1.47", features = ["full"] }

The "full" feature flag includes everything. Yes, it increases compile time. Yes, your binary gets bigger. No, you probably don't need all of it, but figuring out which features you actually need is more annoying than just using "full" until you have a reason to optimize.

For production, you might want to be more specific:

[dependencies]
tokio = { version = "1.47", features = ["net", "rt-multi-thread", "macros"] }

Your First Tokio Program (That Will Crash)

Here's the echo server everyone copies from the docs:

use tokio::net::TcpListener;
use tokio::io::{AsyncReadExt, AsyncWriteExt};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let listener = TcpListener::bind("127.0.0.1:8080").await?;

    loop {
        let (mut socket, _) = listener.accept().await?;
        
        tokio::spawn(async move {
            let mut buf = [0; 1024];
            // This will crash in production - no error handling
            loop {
                let n = socket.read(&mut buf).await.unwrap_or(0);
                if n == 0 { return; }
                socket.write_all(&buf[0..n]).await.unwrap(); // This unwrap will kill your task
            }
        });
    }
}

This example is fine for understanding concepts, but it'll crash the first time someone sends malformed data or the network hiccups. In production, handle your fucking errors.

Runtime Configuration (Matters More Than You Think)

Multi-threaded runtime is the default and what you want 99% of the time:

#[tokio::main] // Multi-threaded, good for most things
async fn main() {}

Single-threaded runtime for debugging or specific constraints:

#[tokio::main(flavor = "current_thread")] // Single-threaded
async fn main() {}

You can also build the runtime manually if you need more control:

let rt = tokio::runtime::Builder::new_multi_thread()
    .worker_threads(4) // Explicit thread count
    .enable_all()
    .build()?;

The Ecosystem You'll End Up Using

Axum - Web framework that doesn't suck. Built by the Tokio team, so it actually integrates properly. Check out the Axum examples.

Hyper - HTTP client and server. Fast but the API is weird. Most frameworks wrap it. See the Hyper guides.

Tonic - gRPC implementation. Actually works, which is rare for gRPC implementations. The Tonic tutorial is decent.

Tracing - Logging for async code. Your println! statements won't help you debug async problems. Read the tracing book.

Production Gotchas

File descriptor limits: Your OS probably limits you to 1024 file descriptors. With thousands of connections, you'll hit this fast. Bump your ulimits or your server will start rejecting connections.

Task memory: Tasks are cheap (~2KB each) but spawning a million of them still uses 2GB of RAM just for task overhead. Don't go crazy.

Blocking calls: Never call `std::thread::sleep` in async code. Use `tokio::time::sleep`. Same goes for any blocking I/O - use `spawn_blocking` or your entire runtime will freeze.

Error handling: Panics in spawned tasks don't crash your program, they just kill that task silently. You'll spend hours debugging "why is my task not running" when it's actually panicking. Use `JoinHandle` to detect task failures.

Questions That Come Up When You Actually Use Tokio

Q

When should I use Tokio instead of something else?

A

Use Tokio if you're building anything that handles network connections or does I/O. The ecosystem is too big to ignore. Use smol if you're building a CLI tool or something simple where you don't need the whole Tokio circus.

Q

How many concurrent tasks can I actually run?

A

Depends on your RAM. I've seen apps with 100K+ connections on a decent server. Each task uses maybe 2KB, so a million tasks is about 2GB just for task overhead. You'll probably hit file descriptor limits before you hit memory limits.

Q

What's the deal with spawn vs spawn_blocking?

A

spawn is for async code that doesn't block. spawn_blocking is for the old synchronous bullshit that will freeze your runtime. If you're calling a database driver that doesn't support async, use spawn_blocking.

Q

Why is my Tokio app using multiple threads?

A

Because that's how Tokio works by default. It spawns one thread per CPU core. If you want single-threaded (for debugging), use #[tokio::main(flavor = "current_thread")].

Q

How do I handle errors without everything exploding?

A

Good luck. Async error handling in Rust is a pain in the ass. Use Result<T, E> and the ? operator like everywhere else, but async stack traces are useless. Tasks that panic just disappear silently, so you'll spend time debugging "why isn't my task running" when it's actually crashing.

Q

Can I mix async runtimes?

A

Don't. Just don't. You'll get deadlocks and weird behavior. Pick Tokio and stick with it.

Q

What Rust version do I need?

A

Tokio 1.47 needs Rust 1.70+. The Tokio team bumps the minimum version regularly, so if you're on ancient Rust, you're fucked.

Q

How do I debug this async nightmare?

A

Use the tracing crate instead of println!. Seriously. Your debug prints won't help you figure out which task is doing what. tokio-console is supposed to help but it's finicky to set up.

Q

Is Tokio good for CPU-heavy stuff?

A

No. Tokio is for I/O. If you're doing heavy computation, use spawn_blocking to move it off the async threads. Or use Rayon for parallelism and call back to Tokio when you need I/O.

Q

How do I call async code from normal code?

A

runtime.block_on(async_function()). But don't call block_on from inside async code

  • you'll deadlock your runtime and hate yourself.
Q

Why does async Rust suck so much?

A

The borrow checker and async don't play nice. You'll get compiler errors about Send and Sync that make no sense. The learning curve is steep and the error messages are cryptic. It's powerful once you get it, but getting there is painful.

Resources That Actually Help

Related Tools & Recommendations

tool
Similar content

Actix Web: Rust's Fastest Web Framework for High Performance

Rust's fastest web framework. Prepare for async pain but stupid-fast performance.

Actix Web
/tool/actix-web/overview
100%
review
Similar content

Rust Web Frameworks 2025: Axum, Actix, Rocket, Warp Performance Battle

Axum vs Actix Web vs Rocket vs Warp - Which Framework Actually Survives Production?

Axum
/review/rust-web-frameworks-2025-axum-warp-actix-rocket/performance-battle-review
98%
tool
Similar content

Axum Web Framework Overview: Why It's Better & How It Works

Routes are just functions. Error messages actually make sense. No fucking DSL to memorize.

Axum
/tool/axum/overview
88%
tool
Similar content

Rust Overview: Memory Safety, Performance & Systems Programming

Memory safety without garbage collection, but prepare for the compiler to reject your shit until you learn to think like a computer

Rust
/tool/rust/overview
72%
compare
Similar content

Python vs JavaScript vs Go vs Rust - Production Reality Check

What Actually Happens When You Ship Code With These Languages

/compare/python-javascript-go-rust/production-reality-check
54%
tool
Similar content

Turso: SQLite Rewritten in Rust – Fixing Concurrency Issues

They rewrote SQLite from scratch to fix the concurrency nightmare. Don't use this in production yet.

Turso Database
/tool/turso/overview
51%
tool
Similar content

WebAssembly: When JavaScript Isn't Enough - An Overview

Compile C/C++/Rust to run in browsers at decent speed (when you actually need the performance)

WebAssembly
/tool/webassembly/overview
51%
tool
Similar content

Crates.io: Official Rust Package Registry & Publishing Guide

The official Rust package registry that works with cargo add and doesn't randomly break your builds like npm

Crates.io
/tool/crates-io/overview
43%
tool
Similar content

Tauri: Build Lightweight Desktop Apps, Ditch Electron Bloat

Explore Tauri, the modern framework for building lightweight, cross-platform desktop apps. Ditch Electron bloat for a fast, efficient development experience. Ge

Tauri
/tool/tauri/overview
38%
tool
Similar content

wasm-pack - Rust to WebAssembly Without the Build Hell

Converts your Rust code to WebAssembly and somehow makes it work with JavaScript. Builds fail randomly, docs are dead, but sometimes it just works and you feel

wasm-pack
/tool/wasm-pack/overview
35%
tool
Similar content

Zed Editor Overview: Fast, Rust-Powered Code Editor for macOS

Explore Zed Editor's performance, Rust architecture, and honest platform support. Understand what makes it different from VS Code and address common migration a

Zed
/tool/zed/overview
35%
tool
Similar content

Lapce: Fast, Lightweight Code Editor - VS Code Alternative

Finally, a VS Code alternative that doesn't need 2GB to edit a text file

Lapce
/tool/lapce/overview
35%
tool
Popular choice

jQuery - The Library That Won't Die

Explore jQuery's enduring legacy, its impact on web development, and the key changes in jQuery 4.0. Understand its relevance for new projects in 2025.

jQuery
/tool/jquery/overview
33%
howto
Similar content

Electron to Tauri Migration Guide: Real-World Challenges

From 52MB to 8MB: The Real Migration Story (And Why It Took Three Weeks, Not Three Days)

Electron
/howto/migrate-electron-to-tauri/complete-migration-guide
31%
alternatives
Similar content

Electron Migration Decision Framework: Should You Switch?

I'm tired of teams agonizing over this choice for months while their Electron app slowly pisses off users

Electron
/alternatives/electron/migration-decision-framework
31%
tool
Similar content

Tauri Security Best Practices: Protect Your App from Exploits

Master Tauri security best practices and configure capabilities to protect your application. Learn to debug common permission issues and prevent exploits in you

Tauri
/tool/tauri/security-best-practices
31%
tool
Similar content

Tauri Mobile Development - Build iOS & Android Apps with Web Tech

Explore Tauri mobile development for iOS & Android apps using web technologies. Learn about Tauri 2.0's journey, platform setup, and current status of mobile su

Tauri
/tool/tauri/mobile-development
31%
howto
Popular choice

Migrate React 18 to React 19 Without Losing Your Sanity

The no-bullshit guide to upgrading React without breaking production

React
/howto/migrate-react-18-to-19/react-18-to-19-migration
31%
howto
Popular choice

Migrate JavaScript to TypeScript Without Losing Your Mind

A battle-tested guide for teams migrating production JavaScript codebases to TypeScript

JavaScript
/howto/migrate-javascript-project-typescript/complete-migration-guide
30%
tool
Similar content

Foundry: Fast Ethereum Dev Tools Overview - Solidity First

Write tests in Solidity, not JavaScript. Deploy contracts without npm dependency hell.

Foundry
/tool/foundry/overview
29%

Recommendations combine user behavior, content similarity, research intelligence, and SEO optimization