# 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.

```rust
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](/docs/benchmarks) show no gain there; measure your own
handlers. Enable the `jemalloc` feature and declare:

```rust
#[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`](https://github.com/compio-rs/compio) runtime, same
  per-worker shape.

The entry points are re-exported from `tako` with the `per-thread` feature:

```rust
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`.

```rust
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](/docs/transports) and
[`examples/proxy-protocol`](https://github.com/rust-dd/tako/tree/main/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](/docs/transports) — protocol-specific spawn
  methods.
* [Observability](/docs/observability) — request IDs, access logs,
  metrics, traces.
* [Runtime compatibility](/docs/concepts/runtimes) — the tokio vs. compio
  split and the per-thread variants.

Source: https://tako.rust-dd.com/docs/deployment