🐙 tako

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 an Arc<Router> that is released when workers exit. Backed by SO_REUSEPORT for kernel-level fan-out across the listeners, with balance_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 the compio runtime, 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

Last updated on

On this page