simoom / Compare / Rust HTTP servers without tokio
No async runtime
Rust HTTP servers without tokio
Most Rust HTTP servers run on tokio. That is a good default, but not every program wants an async runtime: a CLI with a local callback, a desktop app with a dashboard, a device, a function that must start fast. Here are the options that need no tokio, what each is good at, and when tokio is simply the better choice.
The options at a glance
Only simoom and may-minihttp were benchmarked here; tiny_http and rouille were not, so this table gives them no numbers.
| option | model | protocols | good at | trade-offs |
|---|---|---|---|---|
| std::net by hand | whatever you write; usually a thread per connection | what you implement | zero dependencies, total control, learning | parsing, limits, keep-alive and security are all yours to get right |
| tiny_http | synchronous, thread per connection | HTTP/1.1 | a small, easy server for tools and tests | threads get heavy with many connections; few built-in protocols |
| rouille | a framework on tiny_http | HTTP/1.1 | routing and helpers on a synchronous base | inherits tiny_http's model |
| may-minihttp | stackful coroutines (may) | HTTP/1.1 | raw throughput: 6-16% more status requests than simoom in campaign 4 | HTTP/1.1 only; the soundness of stackful coroutines in Rust is debated |
| simoom | one event loop per CPU core; plain functions | HTTP/1.1, HTTP/2, HTTP/3, TLS 1.3, WebSocket, SSE, client | throughput per core, small builds, fast start, zero third-party crates | version 0.1.0; a small API, no middleware ecosystem; TLS not independently audited |
What was measured
Two measurements bear on this choice: an event loop against a thread per connection, and simoom against may-minihttp.
Event loop vs thread per connection
Cameleer's first HTTP server used the design most simple servers share: a thread per connection, on std::net. simoom replaced it, and both were measured on a workstation (WSL2, server on 4 CPUs, 64 connections, closed loop, 2026-11-09):
| thread per connection (cameleer-http) | simoom | |
|---|---|---|
| Throughput | 172,000–173,000 req/s | 293,000–311,000 req/s |
| p99 latency | 0.87–0.91 ms | 0.32–0.44 ms |
| Server CPU per request | about 23 µs | about 13 µs |
| Memory with 1,000 idle connections | 7.7 MB | 2.7 MB |
| Idle memory with TLS | 1,317 KB | 1,360 KB (+3.3%) |
This compares two designs through two specific servers; it is not a measurement of tiny_http or rouille. Details: against the server it replaced.
simoom vs may-minihttp
In benchmark campaign 4 (2026-11-12), with both servers on the same 4-vCPU droplet in the same rounds, may-minihttp answered 158,598 / 154,590 / 155,048 status requests per second with 99% within 1 ms at 64 / 256 / 1,024 clients, against simoom's 137,443 / 145,082 / 133,243: 15%, 6% and 16% more. simoom used 15% less CPU per request (23.4 µs against 28.2 µs), held 35x less memory per idle connection and started 8.9x faster. Full comparison: may-minihttp vs simoom.
How to choose
A test harness or a tiny tool
tiny_http or a few dozen lines on std::net. Simple beats fast when there are three connections.
A raw HTTP/1.1 benchmark server
may-minihttp or simoom's HTTP/1.1-only build. Measure both on your own hardware.
A real service with no async runtime
simoom: HTTP/2, HTTP/3, TLS, WebSocket, limits and a bounded pool for slow work, with no third-party crates.
When to just use tokio
Avoiding tokio is a means, not a goal. axum on tokio is the better choice when:
Use axum or actix-web on tokio when
- Your program already uses async libraries: database drivers, gRPC, message queues, cloud SDKs.
- You want middleware, extractors and integrations off the shelf.
- Your team already knows the async ecosystem.
- You need the production history and contributor base of a widely used framework.
Skip the runtime when
- The program never needed async: a CLI, a desktop app, a daemon, a device.
- Start-up and binary size count: simoom adds about 215 KB for HTTP/1.1, and in campaign 4 its default build answered 0.94 ms after the process started, against 1.48 ms for hyper.
- Every dependency must be reviewed: simoom has none.
- Slow work fits a bounded pool or your own threads (
simoom::blocking,Reply::defer).
Measured against hyper, which axum is built on: hyper and axum vs simoom.
Common questions
Can I write a Rust web server without tokio?
Yes. You can hand-roll one on std::net, use a small synchronous server such as tiny_http or the rouille framework on top of it, use may-minihttp on stackful coroutines, or use simoom, which runs one event loop per CPU core and serves HTTP/1.1, HTTP/2 and HTTP/3 with no async runtime and no third-party crates.
What is the simplest Rust HTTP server?
For a test harness or a tiny tool, tiny_http or a few dozen lines on std::net are the simplest. They give each connection a thread, which is easy to reason about. When you need HTTP/2, TLS, limits or many connections, an engine such as simoom does more of the work for you.
Is a thread per connection good enough?
For a handful of connections, yes. It gets heavy as connections grow. Cameleer's own thread-per-connection server, the one simoom replaced, measured against simoom on a workstation (WSL2, 4 CPUs), used 7.7 MB with 1,000 idle connections and served 172,000-173,000 requests per second; simoom used 2.7 MB and served 293,000-311,000. In benchmark campaign 4 (2026-11-12), on a cloud droplet, the same server held 3,363 bytes per idle connection against simoom's 575.
Should I just use tokio?
Often, yes. If your program already uses async libraries such as database drivers, gRPC or cloud SDKs, axum on tokio fits naturally and has the largest ecosystem. Avoiding tokio pays off when the program never needed async: a CLI, a desktop app, a device, a function that must start fast, or an environment that has to review every dependency.
Do I need async for WebSocket or server-sent events in Rust?
No. simoom serves WebSocket (also over HTTP/2 and HTTP/3) and server-sent events without async: a WebSocket is a handler with an on_message method, and an event stream is a sink you can write to from any thread.
Quick start: three commands
Add the crate, paste the hello-world server from the guide into src/main.rs, and run it.
cargo add simoom cargo run --release curl http://127.0.0.1:8080/