A high-performance HTTP/1.1 server library for Jai, built on Linux epoll with a shared-nothing worker thread pool.
- SO_REUSEPORT workers -- Each worker thread owns its own listen socket and epoll instance. No inter-worker locking or communication. The kernel distributes incoming connections across workers.
- Chi-style router -- Path params (
:name), wildcard segments (*name), middleware chains, sub-router mounting. #add_contextintegration -- Per-requestHTTP_Contextis injected into Jai's implicit context. Handlers access path params viaparam("id")with no explicit context argument.- Per-request pool allocator -- Each worker owns a
Poolthat resets after every request. Handler code usesalloc(),New(), dynamic arrays, etc. with automatic cleanup. - Zero-copy parsing -- HTTP request parser, URL decoder, form parser, and multipart parser all produce string views into the connection read buffer where possible.
- JSON serialization & parsing -- Typed struct ⇄ JSON plus a generic
JSON_Valuetree, vendored from rluba/jaison (MIT). - Batteries included, no install step -- Builds with only the Jai compiler. No package manager and nothing to install at runtime; third-party modules (e.g. JSON) are vendored in-tree, not fetched. Like any Jai program, the resulting binary dynamically links a few system libraries such as libc (visible via
ldd) -- we don't target fully-static musl linking, and that's fine.
Beats nginx on the same hardware. Benchmarked with wrk against a release build serving
"Hello, World!" (16 workers, SO_REUSEPORT, keep-alive, performance governor):
AMD Threadripper 3970X (32C / 64T):
| wrk | jai-http | nginx (control) |
|---|---|---|
| t8 / c500 | 1.03M | 1.03M |
| t16 / c1000 | 1.78M | 1.65M |
| t32 / c2000 | 1.81M | 1.56M |
Intel i7-12800H laptop (20 logical / 6P+8E):
| wrk | jai-http | nginx (control) |
|---|---|---|
| t8 / c500 | 1.53M | 1.35M |
| t16 / c1000 | 1.69M | 1.48M |
| t32 / c2000 | 1.53M | 1.33M |
Earlier builds suffered a high-concurrency throughput collapse, traced to a per-response
temporary-storage allocation in write_response; routing every per-request allocation through the
per-request Pool removed it. Full investigation and a reproducible benchmark environment:
docs/plans/2026-06-19-perf-collapse-investigation.md.
Reproduce with bench.sh (./bench.sh for the server, ./bench.sh nginx for the control).
#import "http_server";
hello_handler :: (request: *Request, response: *Response) {
text(response, "Hello, World!");
}
main :: () {
router: Router;
get(*router, "/", hello_handler);
server: Server;
ok := init_server(*server);
if !ok return;
defer destroy_server(*server);
serve(*server, *router);
server_listen(*server, "0.0.0.0", 9090);
server_run(*server);
}Register routes with get, post, put, http_delete, or the generic route:
router: Router;
get(*router, "/users", list_users);
get(*router, "/users/:id", get_user);
post(*router, "/users", create_user);
// Wildcard -- matches rest of path
get(*router, "/static/*filepath", serve_static);Access path params from any handler:
get_user :: (req: *Request, resp: *Response) {
id, found := param("id");
if !found { resp.status_code = 400; return; }
json(resp, tprint("{\"id\": \"%\"}", id));
}logger :: (ctx: *HTTP_Context) {
// before
proceed(ctx);
// after
}
use(*router, logger);api: Router;
get(*api, "/health", health_check);
use(*api, auth_middleware);
root: Router;
mount(*root, "/api", *api);
// /api/health -> health_check (with auth_middleware)json(resp, "{\"ok\": true}");
text(resp, "plain text");
html(resp, "<h1>Hello</h1>");
redirect(resp, "/login"); // 302
redirect(resp, "/new-url", 301); // 301// Query params: /search?q=hello&page=2
q, _ := query_param(req, "q");
// URL-encoded form body
form := parse_form(req);
name, _ := form_value(*form, "username");
// Multipart form data
data, ok := parse_multipart(req);
file := multipart_value(*data, "avatar");
if file print("filename: %\n", file.filename);Typed serialization and parsing via the vendored json module (rluba/jaison):
#import "json";
Reading :: struct {
station: string;
temp_f: float;
humidity: int;
}
// Serialize a struct to a compact JSON string
r := Reading.{station = "outdoor", temp_f = 72.5, humidity = 48};
body := json_write_string(r, indent_char = "");
// -> {"station": "outdoor","temp_f": 72.5,"humidity": 48}
// Parse a JSON string back into a struct
ok, parsed := json_parse_string(body, Reading);Field names are customized with notes: @JsonName(other_name) to rename, @JsonIgnore to skip.
For payloads whose shape isn't known at compile time, omit the type argument to get a generic
JSON_Value tree instead: ok, value := json_parse_string(body).
Requires the Jai compiler (~/jai/jai/bin/jai-linux). Tested with beta 0.2.029.
Build targets are bare words that resolve to examples/<name>.jai. Debug is the default;
-release flips to optimized. -run runs the single target after building, and ++ forwards
everything after it to that target (implying -run). The passthrough separator is ++ rather
than -- because a standalone -- is reserved by the Jai compiler for its own developer options.
# Build ALL examples (debug) → build_debug/
~/jai/jai/bin/jai-linux first.jai -
# Build one example (debug) → build_debug/hello_world
~/jai/jai/bin/jai-linux first.jai - hello_world
# Optimized build → build_release/hello_world
~/jai/jai/bin/jai-linux first.jai - hello_world -release
# Build + run an example; forward args after `++`
~/jai/jai/bin/jai-linux first.jai - hello_world -run
~/jai/jai/bin/jai-linux first.jai - hello_world ++ --port 9090
# Build and run ALL test suites
~/jai/jai/bin/jai-linux first.jai - run-testsRun a built example:
./build_debug/hello_world # listens on 0.0.0.0:9090All tunables are compile-time module parameters with sensible defaults:
| Parameter | Default | Description |
|---|---|---|
CACHE_LINE_SIZE |
64 | Cache line size for alignment |
READ_BUFFER_SIZE |
4096 | Per-connection read buffer |
MAX_HEADERS |
64 | Max headers per request/response |
MAX_ROUTES |
128 | Max routes per router |
MAX_PARAMS |
8 | Max path params per route |
MAX_MIDDLEWARE |
16 | Max middleware per router |
MAX_MOUNTS |
16 | Max sub-router mounts |
MAX_FORM_VALUES |
64 | Max form fields |
MAX_MULTIPART_PARTS |
16 | Max multipart parts |
LISTEN_BACKLOG |
1024 | TCP listen backlog |
Override at import time:
#import "http_server"(MAX_ROUTES = 256, READ_BUFFER_SIZE = 8192);examples/hello_world.jai -- Example server entry point (build targets: examples/*.jai)
modules/http_server/
module.jai -- Module definition, parameters, imports
http.jai -- Request/Response types, zero-copy parser, serializer
connection.jai -- Connection pool with free list, instance-bit recycling
event.jai -- epoll wrapper, stale event detection
router.jai -- Router, dispatch, middleware chains, #add_context
helpers.jai -- Response helpers, URL decode, query/form/multipart parsing
server.jai -- Worker threads, SO_REUSEPORT, event loop
modules/json/ -- Vendored JSON (rluba/jaison, MIT): typed + generic interfaces
modules/datetime/ -- RFC3339 parsing, formatting, Unix epoch, bucketing
modules/channel/ -- Generic blocking queue Channel(T) for the actor pattern
modules/csv/ -- Compile-time-validated CSV read/write (RFC 4180)
MIT