fasthttp
fasthttp is a low-level, high-performance HTTP/1.1 server for V built on
platform-native I/O multiplexing. It gives you raw control over request bytes and
response bytes with a small, explicit API, and is the parallel backend used by
veb.
Features
- Native I/O multiplexing:
epollon Linux,kqueueon macOS/BSD, IOCP on Windows. On Linux each worker owns its ownSO_REUSEPORTlistener (kernel load balancing across cores). - Zero per-request allocation on the hot path (Linux): each connection reuses
its normal-sized read and write buffers. Oversized buffers above 64 KiB shrink
after their contents are consumed. Each worker retains at most 16 closed
connection states for reuse, releasing additional states after traffic bursts. With
-prealloc, individual allocations cannot be reclaimed, so workers retain all connection states and buffer capacities for reuse instead of repeatedly replacing them. - HTTP/1.1 pipelining: several requests arriving in one read are framed individually and answered into one batched write. TCP-fragmented requests are reassembled deterministically via exact-length framing.
- Two handler contracts:
- Append handler (recommended): write the raw response straight into the connection's reused buffer — no response object, no copy.
- Classic handler: build and return an
HttpResponse.
- Lock-free per-worker state: an optional
make_statehook gives each worker thread its own state (a DB connection, a reused scratch buffer) with no mutex. - Takeover: hand a connection off to your own code (SSE / WebSocket) or write the response yourself and keep the connection alive.
- Zero-copy file bodies: return a
file_pathand the body is streamed withsendfile(2). - Graceful shutdown: drain in-flight responses, then stop.
Linux workers collect up to 256 epoll events per wait. Each worker reserves only that many event slots; the listening backlog and connection capacity remain independent of the batch size. Timeout sweeps run at most once per 100 ms, independently of the number of ready event batches.
Installation
Part of the standard library:
import fasthttp
Quick start (append handler)
The append handler appends the complete raw HTTP response (status line + headers
- body) into the reused
outbuffer and returns aStep. It is the zero-copy, pipelining-friendly contract.
import fasthttp
fn handle(req fasthttp.HttpRequest, mut out []u8, ws voidptr, mut ctl fasthttp.ResponseControl) fasthttp.Step {
path := req.buffer[req.path.start..req.path.start + req.path.len].bytestr()
if path == '/' {
out << 'HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 13\r\n\r\nHello, World!'.bytes()
} else {
out << 'HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\n\r\n'.bytes()
}
return .done
}
fn main() {
mut server := fasthttp.new_server(fasthttp.ServerConfig{
port: 3000
append_handler: handle
}) or {
eprintln('failed to create server: ${err}')
return
}
println('listening on http://localhost:3000/')
server.run() or { eprintln('error: ${err}') }
}
Quick start (classic handler)
The classic handler builds and returns an HttpResponse. It is simpler when you
already have the response bytes in hand.
import fasthttp
fn handle(req fasthttp.HttpRequest) !fasthttp.HttpResponse {
return fasthttp.HttpResponse{
content: 'HTTP/1.1 200 OK\r\nContent-Length: 13\r\n\r\nHello, World!'.bytes()
}
}
fn main() {
mut server := fasthttp.new_server(fasthttp.ServerConfig{
port: 3000
handler: handle
}) or {
eprintln('failed to create server: ${err}')
return
}
server.run() or { eprintln('error: ${err}') }
}
Set exactly one of handler or append_handler; new_server returns an
error otherwise.
Reading the request
HttpRequest exposes zero-copy Slices (start, len) into buffer:
method := req.buffer[req.method.start..req.method.start + req.method.len].bytestr()
path := req.buffer[req.path.start..req.path.start + req.path.len].bytestr()
body := req.buffer[req.body.start..req.body.start + req.body.len]
The parser fills method, path, version, header_fields and body. The
request-framing helpers (frame_request_length, frame_expected_total,
frame_head_len) are the pure functions the read loop uses to split pipelined
requests; they are exported for testing and for building your own reader.
The append-handler contract
pub type AppendHandler = fn (req HttpRequest, mut out []u8, worker_state voidptr, mut ctl ResponseControl) Step
out— the connection's persistent write buffer. Append the complete raw HTTP response. Everything appended during one readiness event is sent in a single write; the buffer is reused across requests — never free it or keep a reference.worker_state— the valueServerConfig.make_statereturned on this worker thread (see below);nilif unset.ctl ResponseControl— out-of-band controls:takeover_mode—.manualhands the fd off (you own it, e.g. SSE/WebSocket);.reusablemeans you wrote the response yourself but want keep-alive.should_close— close the connection after this response.file_path— stream this file after the appended bytes (sendfile).
- Return
Step:.done— response complete inout; keep the connection alive..close— sendout, then close..suspend— reserved for future async handlers (no watch reactor yet, so it currently drops the connection).
Lock-free per-worker state
ServerConfig.make_state is called once per worker thread; its return value
reaches every request on that worker as worker_state. Because each worker gets
its own instance, no locking is needed:
struct WorkerState {
mut:
scratch []u8 // reused per-request render buffer
}
fn make_state() voidptr {
return &WorkerState{}
}
fn handle(req fasthttp.HttpRequest, mut out []u8, ws voidptr, mut ctl fasthttp.ResponseControl) fasthttp.Step {
mut st := unsafe { &WorkerState(ws) }
st.scratch.clear()
// ... build into st.scratch, then `out << st.scratch` ...
return .done
}
// fasthttp.ServerConfig{ ..., append_handler: handle, make_state: make_state }
Configuration
ServerConfig fields: family (.ip / .ip6), port,
max_request_buffer_size (bounds the request head; an oversized head gets 431),
max_request_body_size (defaults to 64 MiB; set to 0 only to allow unlimited buffered
request bodies), timeout_in_seconds (read/write deadlines; a stalled request gets 408),
user_data (an opaque pointer surfaced as HttpRequest.user_data), handler /
append_handler, and make_state.
Lifecycle
mut server := fasthttp.new_server(config)!
handle := server.handle()
spawn server.run()
handle.wait_till_running()! // block until the listener is bound
// ... serve ...
handle.shutdown(timeout: 5 * time.second)! // drain in-flight, then stop
Platform support
| Platform | Backend | Pooling + pipelining | Append handler | make_state |
|---|---|---|---|---|
| Linux | epoll | yes | yes | yes |
| macOS/BSD | kqueue | yes, serial responses | yes | yes |
| Windows | IOCP | yes, serial responses | yes | yes |
Request-scoped allocation with -prealloc
When compiled with -prealloc, the classic handler path runs each request
inside a scoped bump arena, freed as a unit after the response is sent. The
append handler path does not open a reactor arena (growing the reused write
buffer inside a scope would free it out from under the connection); an append
handler that wants request-scoped arenas manages its own and leaves it before
writing into out. To trace arena usage:
v -prealloc -d trace_prealloc run .
Example
See examples/fasthttp/ for a small multi-route server:
./v run examples/fasthttp