Nyx by Example

Serve Multi-threaded

http_serve_mt spawns N worker threads that pull connections from a shared channel. This achieves 73K req/s on a 4-core ARM64 — comparable to Go's net/http.

Code

// nyx-serve multi-threaded — http_serve_mt for 73K req/s
// nyx-serve multi-hilo — http_serve_mt alcanza 73K req/s

import "std/http"

fn on_request(req: Array) -> String {
    let method: String = req[1]
    let path: String = req[2]

    if path == "/" {
        return http_response(200, "Hello from worker thread!")
    }
    if path == "/health" {
        return http_response(200, "{\"status\":\"ok\"}")
    }
    return http_response(404, "not found")
}

fn main() -> int {
    // http_serve_mt spawns N worker threads.
    // A channel distributes accepted connections to workers.
    // Workers parse requests and call the handler in parallel.
    let port: int = 8080
    let workers: int = 8  // typically: number of CPU cores

    print("multi-threaded server on :" + int_to_string(port))
    print("worker threads: " + int_to_string(workers))
    print("benchmark: 73K req/s on 4-core ARM64")

    // Blocks forever, accepting and dispatching connections
    http_serve_mt(port, workers, on_request)
    return 0
}

Output

multi-threaded server on :8080
worker threads: 8
benchmark: 73K req/s on 4-core ARM64

Explanation

Single-threaded accept loops hit a ceiling fast: the kernel can fan out connections faster than one goroutine can parse them. http_serve_mt solves this with the classic producer-consumer pattern — one accept thread feeds a bounded channel, N worker threads drain it. Each worker owns its own parser state, so there's zero contention on the hot path. On a 4-core ARM64 with 8 workers, Nyx hits 73K req/s on plain HTTP responses — that's in the same league as Go's net/http and well ahead of Node.js. The GC is thread-aware (GC_THREADS), so allocations inside handlers are safe without manual synchronization.

← Previous Next →

Source: examples/by-example/82-serve-multithread.nx