Cameleeringsimoom

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.

5ways to serve HTTP without an async runtimestd::net, tiny_http, rouille, may-minihttp, simoom
1.7–1.8xsimoom against a thread-per-connection serveron a workstation, against the server it replaced
2.7 vs 7.7 MBwith 1,000 idle connectionssimoom vs the thread-per-connection server it replaced, same workstation
0third-party crates in simoomstd only

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.

optionmodelprotocolsgood attrade-offs
std::net by handwhatever you write; usually a thread per connectionwhat you implementzero dependencies, total control, learningparsing, limits, keep-alive and security are all yours to get right
tiny_httpsynchronous, thread per connectionHTTP/1.1a small, easy server for tools and teststhreads get heavy with many connections; few built-in protocols
rouillea framework on tiny_httpHTTP/1.1routing and helpers on a synchronous baseinherits tiny_http's model
may-minihttpstackful coroutines (may)HTTP/1.1raw throughput: 6-16% more status requests than simoom in campaign 4HTTP/1.1 only; the soundness of stackful coroutines in Rust is debated
simoomone event loop per CPU core; plain functionsHTTP/1.1, HTTP/2, HTTP/3, TLS 1.3, WebSocket, SSE, clientthroughput per core, small builds, fast start, zero third-party cratesversion 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
Throughput172,000–173,000 req/s293,000–311,000 req/s
p99 latency0.87–0.91 ms0.32–0.44 ms
Server CPU per requestabout 23 µsabout 13 µs
Memory with 1,000 idle connections7.7 MB2.7 MB
Idle memory with TLS1,317 KB1,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/