Skip to content

v2.4.0

Latest

Choose a tag to compare

@dancixx dancixx released this 01 Oct 10:13
· 1 commit to main since this release
d12a271

Tako 2.4.0 makes the HTTP/1 request path much cheaper. A hello-world request on a per-thread worker's keep-alive connection no longer allocates, routes without middleware skip the dispatch pipeline, and HTTP/1 connections set no timer per request. The per-thread server now serves 7% more requests than Actix Web when the server is the bottleneck and 17% more with pipelining, and the default server serves about 24% more than Axum.

No code changes are needed. Three behaviours change; read the 2.4 migration guide before upgrading from 2.3.x. All eight workspace crates are versioned together at 2.4.0 and require Rust 1.95 or newer.

Behaviour changes

  • PerThreadConfig::pin_to_core now defaults to false. Every process pinned from core 0, so per-thread servers on one machine competed for the same cores, and a pinned worker could not move off a busy core. Set pin_to_core: true to pin on a machine the server has to itself.
  • On the Tokio server's plain HTTP/1 listener and on per-thread workers, header_read_timeout is checked every half deadline: an idle connection closes between 1 and 1.5 times the timeout after its last request, instead of exactly at it. TLS, Unix socket, vsock, and PROXY protocol listeners keep the exact timer.
  • A handler hands the request's extension map back for reuse once its last extractor has run, so values that middleware put into the request extensions are dropped before the handler body runs. Middleware that needs a guard, such as a semaphore permit, to live until the handler finishes should hold it itself.

Added

  • FromRequest::ENTRIES and FromRequestParts::ENTRIES, of the new tako::extractors::Entries type, list the framework entries an extractor reads, such as the matched path, the path parameters, or the connection info. A route without middleware or other per-request hooks attaches only the entries its handler's extractors list. The default, Entries::ALL, keeps existing extractors working unchanged, and every built-in extractor lists what it reads.

Performance

  • Request header and extension maps are reused on the thread that served them, and responses are built on recycled header maps. A hello-world request on a per-thread worker's keep-alive connection makes no heap allocation, down from 11 in 2.3.0.
  • Routes without middleware, timeouts, guards, signal listeners, or error hooks start their handler right after the route lookup.
  • Small handler futures are stored inline instead of boxed.
  • Plain HTTP/1 and per-thread connections check for shutdown with one atomic load, and the header deadline needs no timer work per request.
  • The per-thread accept loop runs as its own task, so it is no longer polled each time a connection wakes.
  • At 16 connections a per-thread hello-world request takes 5,928 instructions, down from 9,527 in 2.3.0 (callgrind).

Hello-world requests per second, the median of five 15-second wrk runs in a 24 vCPU container on an AMD EPYC 9655P host (methodology on the benchmarks page):

Framework Loopback, 1,000 conns Server-bound, 256 conns Pipelined ×16, 256 conns
Tako per-thread 1,842,063 612,091 15,033,668
Tako per-thread, pinned 1,780,810 611,956 15,716,822
Actix Web 1,786,640 570,865 12,806,477
Tako 1,401,625 483,526 11,512,467
Axum 1,132,259 389,844 —

Against 2.3.0 in the same run, each with its default settings, the default server serves 20% more requests at 1,000 connections, 21% more server-bound, and 53% more pipelined, and the per-thread server 9% more server-bound and 30% more pipelined. Axum has no pipelined number because axum::serve leaves TCP_NODELAY off.

Documentation

  • A Writing an extractor guide with ENTRIES, the 2.4 migration guide, benchmarks with server-bound and pipelined loads, and deployment notes on core pinning and jemalloc.

Full diff: v2.3.0...v2.4.0