|claim|login
RepoCritics — Review. Share. Archive. Every open-source repo.

smol-rs/smol

Wiki: smol-rs/smol

Source: https://github.com/smol-rs/smol

Last synced 2026-07-17 · 995 words · Edit wiki on GitHub →

smol-rs/smol

> A small, composable async runtime for Rust that is mostly a thin facade over a family of independently useful crates.

GitHub repo · docs.rs · License: Apache-2.0 OR MIT

Overview

smol is an async runtime for Rust originally written by Stjepan Glavina (also author of async-std and much of crossbeam) and first published in early 20201. Its distinguishing design choice is stated plainly in the README: the smol crate "simply re-exports other smaller async crates." The real substance lives in a dozen independently versioned crates under the smol-rs org — async-executor, async-io, polling, async-channel, blocking, futures-lite, and others — that smol bundles into one ergonomic surface.

This modularity is the project's defining tension. The component crates are so generally useful that they are depended upon far beyond smol itself: polling, async-channel, event-listener, and concurrent-queue show up transitively in a large share of the async Rust ecosystem, including projects that otherwise run on Tokio. So smol's footprint is much larger than its star count suggests — but the smol facade crate is a minority runtime choice. Most libraries (axum, tonic, reqwest, sqlx defaults) assume Tokio, and using them under smol requires the async-compat shim, which starts a Tokio runtime alongside smol's reactor2.

The project is community-maintained under the smol-rs organization after Glavina stepped back, with John Nunley (notgull) the most active core-crate maintainer in recent years. The stated goal is a small, auditable, fast-compiling async stack rather than a maximal-throughput one.

Getting Started

cargo add smol

```rust,no_run use smol::{io, net, prelude::*, Unblock};

fn main() -> io::Result<()> { smol::block_on(async { let mut stream = net::TcpStream::connect("example.com:80").await?; let req = b"GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n"; stream.write_all(req).await?;

// Unblock offloads the blocking stdout handle onto a thread pool. let mut stdout = Unblock::new(std::io::stdout()); io::copy(stream, &mut stdout).await?; Ok(()) }) }


`smol::block_on` drives a future to completion on the current thread;
`smol::spawn` schedules onto a lazily-started global multi-threaded executor.
For a proc-macro-free `async fn main` or an explicit executor, use the
companion `smol-macros` crate rather than reaching for `#[tokio::main]`-style
attributes.

## Architecture / How It Works

`smol` is a facade; understanding it means understanding the layers underneath:

- **`polling`** — a portable wrapper over the OS readiness APIs (epoll on Linux,
  kqueue on the BSDs/macOS, event ports on illumos/Solaris, IOCP/wepoll on
  Windows). This is the bottom of the I/O stack.
- **`async-io`** — a single reactor thread built on `polling` that registers
  file descriptors and timers, plus the `Async<T>` adapter that turns any
  `AsRawFd`/`AsRawSocket` type into a future-aware one. Timers live here too.
- **`async-executor`** — the task scheduler. `Executor` is a multi-threaded,
  work-stealing-ish executor; `LocalExecutor` is single-threaded. `smol::spawn`
  targets a lazily-initialized global `Executor`.
- **`blocking`** — a dynamically-sized thread pool for work that has no async
  equivalent. `Unblock<T>` and the `unblock()` function move blocking I/O onto
  it.
- **`async-fs` / `async-net` / `async-process`** — filesystem, socket, and
  subprocess wrappers layered on the above; **`async-channel` / `async-lock` /
  `event-listener`** provide channels, locks, and the notification primitive
  they are built on; **`futures-lite`** is a lighter reimplementation of the
  common `futures` surface that keeps dependency and compile costs down.

A key architectural consequence: `async-fs` is **not** kernel-async I/O — it is
ordinary blocking `std::fs` calls dispatched onto the `blocking` thread pool
(io_uring-style async disk I/O is not part of this stack). Networking and
timers, by contrast, are true readiness-based async through the `polling`
reactor. Reactor and executor are decoupled, which is what lets you mix
`block_on`, a global `spawn`, and hand-built `Executor` instances in one
program.

## Production Notes

- **Ecosystem lock-in is the main cost.** Most async libraries are written
  against Tokio's traits and runtime handle. Running them under `smol` means
  wrapping their futures/streams in `async-compat`, which spawns a Tokio runtime
  next to `smol`'s reactor — two runtimes in one process, with the attendant
  overhead and reasoning burden[^2]. If your dependency tree is Tokio-heavy,
  `smol` fights you.
- **"Async" filesystem is thread-pool offload.** Heavy `async-fs` workloads are
  bounded by the `blocking` pool's thread count and by page-cache/syscall
  latency, not by any kernel async path. Do not expect io_uring-class disk
  throughput.
- **Single global reactor thread.** All `async-io`-registered I/O and timers are
  serviced by one reactor thread. It is efficient for typical workloads but is a
  different scaling shape from Tokio's per-worker I/O drivers.
- **No macro runtime by default.** There is no `#[smol::main]`. This is
  deliberate (fast compiles, no proc-macro dependency), but it surprises people
  migrating from Tokio; use `smol-macros` or write `fn main` + `block_on`.
- **You are already using parts of it.** Tokio-based builds commonly pull in
  `polling`, `async-channel`, or `event-listener` transitively, so `smol-rs`
  crates in your `Cargo.lock` do not mean someone chose the `smol` runtime.
- **Conservative MSRV.** 1.85, informally pinned to the Rust in Debian Stable
  and advanced only for major ecosystem shifts or security fixes.

## When to Use / When Not

**Use when:**
- You want a small, auditable async stack with fast compile times and a shallow
  dependency tree.
- You are embedding async into a mostly-synchronous program and want
  `block_on` plus occasional `spawn` without adopting a large runtime.
- You want to compose executors and reactors explicitly rather than accept a
  monolithic runtime's defaults.

**Avoid when:**
- Your stack leans on Tokio-native libraries (axum, tonic, tower, most gRPC/DB
  clients) — the `async-compat` tax is not worth it.
- You need io_uring-class disk or completion-based network I/O.
- You want the largest ecosystem of runtime-specific middleware, tracing
  integrations, and hiring familiarity; that is Tokio.

## Alternatives

- tokio-rs/tokio — the default choice; use it when you need the ecosystem, the
  tooling, or io-driver throughput at scale.
- async-rs/async-std — a std-shaped API built on the same `smol-rs` crates, but
  deprecated in 2025; new projects should not start here.
- compio-rs/compio — use instead when you specifically want completion-based
  (io_uring / IOCP) async I/O.
- DataDog/glommio — use for thread-per-core, io_uring, high-throughput Linux
  network services.
- embassy-rs/embassy — use for `no_std` embedded async on microcontrollers,
  where `smol`'s OS-reactor model does not apply.

## History

| Version | Date | Notes |
|---------|------|-------|
| repo created | 2020-02-04 | `smol-rs/smol` first pushed[^1]. |
| 0.1 | 2020 | Initial releases; single-crate experiment by Stjepan Glavina. |
| 1.0 | 2020 | Stabilized facade re-exporting the component crates. |
| 2.0 | 2023 | Breaking release aligning with newer `async-io`/component majors[^3]. |
| — | 2026-06 | Actively maintained under `smol-rs`; last push 2026-06-27, MSRV 1.85. |

## References

[^1]: `smol-rs/smol` repository metadata (created 2020-02-04, 5,011 stars,
Apache-2.0 OR MIT). https://github.com/smol-rs/smol
[^2]: `async-compat` — adapter for running Tokio-based futures and I/O under
other runtimes. https://docs.rs/async-compat
[^3]: `smol` release history on crates.io. https://crates.io/crates/smol/versions

## Tags

rust, async, async-runtime, futures, executor, networking, concurrency, io, event-loop, minimalist