🐙 tako
Reference

Migrating to 2.4

Tako 2.4 needs no code changes; review the core pinning default, the coarser HTTP/1 header deadline, and the extractor ENTRIES list.

Tako 2.4.0 makes the HTTP/1 request path cheaper and needs no code changes. Per-thread workers no longer pin to cores by default, one timeout behaves slightly differently, request extensions are dropped earlier, and custom extractors can opt into the cheaper path. See the release notes for the complete list.

Per-thread workers no longer pin to cores

PerThreadConfig::pin_to_core now defaults to false. A pinned worker cannot move off a core that another process keeps busy, and every process pins from core 0, so two per-thread servers on one machine competed for the same cores. To keep the 2.3 behaviour on a machine the server has to itself, set it back:

use tako::PerThreadConfig;

let config = PerThreadConfig {
  pin_to_core: true,
  ..PerThreadConfig::default()
};

The header deadline is checked every half deadline

On the Tokio server's plain HTTP/1 listener (spawn_http and the serve functions) and on per-thread workers, header_read_timeout no longer starts a timer for every request. Each connection checks it every half deadline instead, so a connection that stops sending request heads closes between 1 and 1.5 times header_read_timeout after it went idle, rather than exactly at it. A connection that is still running a handler or streaming a response is never closed by it. TLS, Unix socket, vsock, and PROXY protocol listeners keep the exact timer.

Extractors list the entries they read

FromRequest and FromRequestParts gain an associated ENTRIES constant of the new tako::extractors::Entries type. On a route without middleware or other per-request hooks, the router attaches only the entries that the handler's extractors list, such as the matched path or the connection info. The default, Entries::ALL, keeps existing extractors working unchanged; see Writing an extractor to list fewer.

Request extensions are dropped before the handler runs

A handler hands the request's header and extension maps back for reuse as soon as its last extractor has run. A value that middleware put into the request extensions is therefore dropped before the handler body runs, not after it returns. Values that extractors read, such as a session or JWT claims, are not affected, because the extractor keeps its own copy. A middleware that stores a guard there, such as a semaphore permit, and needs it to live until the handler finishes should hold the guard itself:

use std::sync::Arc;
use tokio::sync::Semaphore;

let limit = Arc::new(Semaphore::new(64));
router.middleware(move |req, next| {
  let limit = Arc::clone(&limit);
  async move {
    let _permit = limit.acquire_owned().await;
    next.run(req).await
  }
});

Performance changes

These need no code changes:

  • Routes without middleware or other per-request hooks take a shorter dispatch path.
  • Request header and extension maps are reused on the thread that served them, and small handler futures are stored inline. A hello-world request on a per-thread worker's keep-alive connection makes no heap allocation, down from 11 in 2.3.
  • Plain HTTP/1 and per-thread connections check for shutdown with one atomic load and set no timer per request.
  • The per-thread accept loop runs as its own task, so it is no longer polled each time a connection wakes.

Against 2.3.0 in the same benchmark run, the default server serves 20% more hello-world requests at 1,000 connections and 53% more pipelined, and per-thread workers serve 9% more when the server is the bottleneck and 30% more pipelined, each with its default settings.

Last updated on

On this page