Deployment
The three tako deployment shapes — single binary, thread-per-core, and behind a load balancer — plus socket activation and graceful shutdown.
Tako accommodates three deployment shapes, plus a few production niceties (socket activation, PROXY protocol fronting, per-thread runtimes). Pick the shape that matches your infrastructure; you can use the same router and handler code across these deployment shapes.
use std::time::Duration;
use tako::{Server, ServerConfig};
use tako::router::Router;
use tokio::net::TcpListener;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let listener = TcpListener::bind("0.0.0.0:8080").await?;
let mut router = Router::new();
router.get("/", || async { "hi" });
let server = Server::builder()
.config(ServerConfig {
drain_timeout: Duration::from_secs(30),
max_connections: Some(50_000),
..ServerConfig::default()
})
.build();
let handle = server.spawn_http(listener, router);
tako::shutdown_signal().await?;
handle.shutdown(Duration::from_secs(30)).await;
Ok(())
}Single binary (the default)
Server::builder + tokio multi-threaded runtime. max_connections
bounds accept-spawn pressure; drain_timeout controls the grace
period for graceful shutdown. This is the shape most services need.
Consider jemalloc as the global allocator when handlers allocate. Connection
tasks move between worker threads, so memory is often freed on a different
thread than the one that allocated it, a pattern jemalloc usually handles
better than the system allocator. A hello-world request does not allocate since
2.4, so the benchmarks show no gain there; measure your own
handlers. Enable the jemalloc feature and declare:
#[global_allocator]
static GLOBAL: tako::Jemalloc = tako::Jemalloc;Thread-per-core
Two flavours, both behind cargo features:
per-thread— N current-thread tokio runtimes, one per core, sharing anArc<Router>that is released when workers exit. Backed bySO_REUSEPORTfor kernel-level fan-out across the listeners, withbalance_connections(on by default) handing each new connection to the worker with the fewest live connections.per-thread-compio— io_uring (Linux), IOCP (Windows), or kqueue (macOS) via thecompioruntime, same per-worker shape.
The entry points are re-exported from tako with the per-thread feature:
use tako::{PerThreadConfig, serve_per_thread};
use tako::router::Router;
fn main() -> std::io::Result<()> {
let mut router = Router::new();
router.get("/", || async { "hello from per-thread" });
let config = PerThreadConfig { workers: 4, ..PerThreadConfig::default() };
serve_per_thread("0.0.0.0:8080", router, config)
}serve_per_thread waits for Ctrl+C or SIGTERM. For explicit control, use
spawn_per_thread(addr, router, config), await
shutdown.wait_for_bind_outcome(worker_count), then call shutdown.trigger()
and join the returned OS thread handles during teardown. Bind and plugin setup
errors are reported through the shutdown handle. These workers currently serve
HTTP/1.1. TLS, h2c, and PROXY listeners use the regular server builder.
Workers are not pinned to CPU cores by default, so the OS can move a worker off
a core that another process keeps busy. On a machine the server has to itself,
pinning keeps each worker's caches warm; set pin_to_core: true and worker n
runs on the n-th CPU core. The affinity feature that does the pinning comes
with per-thread.
let config = PerThreadConfig {
workers: 4,
pin_to_core: true,
..PerThreadConfig::default()
};per-thread-compio selects Compio consistently for workers, middleware,
signals, and queues. Kernel load balancing with SO_REUSEPORT is intended for
Linux. On macOS, where the kernel sends every connection to one listener, Tokio
workers still share the load through balance_connections; the Compio flavour
does not balance connections, so run it with one worker there.
Behind a load balancer
L4 proxies that prepend a PROXY v1/v2 header (HAProxy, AWS NLB, fly.io
edges) are supported by Server::spawn_proxy_protocol with proxy-protocol enabled. The parser is
TLV-aware and CRC32C-verified, and rewrites X-Forwarded-* from the
parsed header so handlers see the real client address. See the
Transports overview and
examples/proxy-protocol.
L7 HTTP/2 proxies (Envoy, Nginx) that terminate TLS and forward
cleartext h2c upstream use Server::spawn_h2c — see the same chapter
for an h2c example.
Socket activation
Behind the socket-activation cargo feature. LISTEN_FDS /
LISTEN_PID (and the s6 / catflap equivalents) are read by
tako::socket_activation::ListenFds::from_env(). The returned
listener types feed the existing Server::spawn_* methods, so the
same handler code runs under systemd, runit, or supervised launch
without source changes.
Router ownership
Listeners and their connection tasks own Arc<Router> references. Graceful
shutdown stops accepting, drains active work within drain_timeout, then
cancels remaining tasks. A runtime router-swap API is not provided;
ROUTER_HOT_RELOAD is reserved for explicit application signals.
Cross-references
- Transports overview — protocol-specific spawn methods.
- Observability — request IDs, access logs, metrics, traces.
- Runtime compatibility — the tokio vs. compio split and the per-thread variants.
Last updated on