fasthttp #
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 owns a read buffer and a write buffer that are reused for its whole lifetime, and closed connections return their state — buffers included — to a per-worker free-list. No per-connection hash maps, no per-request buffer churn.
- 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 theconnection'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.
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 itcurrently 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 413), 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 | one request per read | yes | yes |
| Windows | IOCP | one request per read | yes | not yet (run WIP) |
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
fn decode_http_request #
fn decode_http_request(buffer []u8) !HttpRequest
decode_http_request parses a raw HTTP request from the given byte buffer
fn frame_expected_total #
fn frame_expected_total(buf []u8) int
frame_expected_total returns the full HTTP/1.1 message length (headers + body) as soon as it is determinable from the bytes buffered so far: the header section must be complete AND the body length known via Content-Length. Returns -1 when not yet determinable — headers incomplete, a chunked body (length unknown until the terminator), or no Content-Length at all.
This is a pure sizing HINT for the read loop: it lets a large upload grow its recv buffer to the exact message size in ONE allocation instead of doubling toward it. The authoritative framing and limit checks stay in frame_request_length_lim, which the read loop still runs once the bytes arrive.
fn frame_head_len #
fn frame_head_len(buf []u8) int
frame_head_len returns the byte offset where the body begins — the length of the request head (request line + header section + the terminating CRLFCRLF) — or -1 if the head is not yet complete in buf.
fn frame_request_length #
fn frame_request_length(buf []u8) !int
frame_request_length inspects the bytes received so far and returns: -1 -> incomplete; read more bytes total >= 0 -> a complete message occupying exactly total bytes is present It errors only on genuinely malformed framing (map to 400). Body length comes from Content-Length, or from chunked decoding (Transfer-Encoding), or is zero.
fn frame_request_length_lim #
fn frame_request_length_lim(buf []u8, max_header int, max_body int) !int
frame_request_length_lim is frame_request_length with optional size limits (0 = unlimited, zero-cost). When a limit is exceeded it returns an error whose .code() is the HTTP status to send: 431 (header fields too large) or 413 (payload too large). Other malformed framing carries code 400. Thin Result wrapper over the no-Result hot-path twin frame_request_length_lim_idx: cold callers (tests, decode) keep this API, while the per-request drain loop calls the twin directly to skip the !int boxing.
fn frame_request_length_lim_idx #
fn frame_request_length_lim_idx(buf []u8, max_header int, max_body int) int
frame_request_length_lim_idx is the no-Result hot-path twin of frame_request_length_lim: it returns a plain int and never constructs a Result, so the per-request success path skips the !int boxing. Returns a length >= 0 (complete — exactly that many bytes), -1 (incomplete — wait for more bytes), or a frame_err_* sentinel that the Result wrapper maps to 400 / 413 / 431.
fn new_server #
fn new_server(config ServerConfig) !&Server
new_server creates and initializes a new Server instance.
fn parse_http1_request_line #
fn parse_http1_request_line(mut req HttpRequest) !int
parse_http1_request_line parses the request line of an HTTP/1.1 request. spec: https://datatracker.ietf.org/doc/rfc9112/ request-line is the start-line for for requests According to RFC 9112, the request line is structured as: request-line = method SP request-target SP HTTP-version where: METHOD is the HTTP method (e.g., GET, POST) SP is a single space character REQUEST-TARGET is the path or resource being requested HTTP-VERSION is the version of HTTP being used (e.g., HTTP/1.1) CRLF is a carriage return followed by a line feed returns the position after the CRLF on success
fn ResponseTakeoverMode.from #
fn ResponseTakeoverMode.from[W](input W) !ResponseTakeoverMode
fn Step.from #
fn Step.from[W](input W) !Step
type AppendHandler #
type AppendHandler = fn (req HttpRequest, mut out []u8, worker_state voidptr, mut ctl ResponseControl) Step
AppendHandler is the zero-copy request handler contract: it appends the raw HTTP response into the connection's reused write buffer out (rather than allocating and returning a response), reads per-worker state via worker_state (see ServerConfig.make_state), signals connection handling through ctl, and returns a Step. Appending into out — which the reactor reuses across requests and flushes for every pipelined request in one send — removes the per-request response allocation that the return-a-response handler contract requires.
enum ResponseTakeoverMode #
enum ResponseTakeoverMode {
none
manual
reusable
}
enum Step #
enum Step {
done
close
suspend
}
Step is what an append-style handler (see ServerConfig.append_handler) returns to the reactor, mirroring vanilla's handler contract: .done — the response is complete in out; the reactor sends it and keeps the connection alive (unless ResponseControl.should_close is set). .close — the reactor sends whatever is in out, then closes the connection. .suspend — reserved for async handlers that park on an external fd. There is no watch reactor yet, so a .suspend currently drops the connection (like vanilla's reactorless backends); it is defined now so the contract is stable when async support lands.
struct HttpRequest #
struct HttpRequest {
pub mut:
buffer []u8 // A V slice of the read buffer for convenience
method Slice
path Slice
version Slice
header_fields Slice
body Slice
client_conn_fd int
client_conn_handle usize
user_data voidptr // User-defined context data (shared, set from ServerConfig.user_data)
// worker_state is the value ServerConfig.make_state returned on THIS worker
// thread (nil when no make_state is configured). It is thread-local by
// construction — one instance per worker thread — so a handler can keep
// per-worker resources (a DB connection, a reused render scratch buffer)
// without any locking: `unsafe { &MyState(req.worker_state) }`.
worker_state voidptr
}
HttpRequest represents an HTTP request. TODO make fields immutable
struct HttpResponse #
struct HttpResponse {
pub mut:
content []u8
file_path string
takeover_mode ResponseTakeoverMode
should_close bool // if true, close the connection after sending (Connection: close)
// content_owned lets the backend free or move content after it has been sent.
content_owned bool
// request_arena is a prealloc scope handle that must be freed after sending.
request_arena voidptr
}
struct ResponseControl #
struct ResponseControl {
pub mut:
// takeover_mode lets the handler take over the socket instead of having the
// reactor send `out`: .manual hands the fd off entirely (SSE/WebSocket), and
// .reusable means the handler wrote the response itself but wants the reactor
// to keep serving the (kept-alive) connection.
takeover_mode ResponseTakeoverMode
// should_close asks the reactor to close the connection after this response.
should_close bool
// file_path, when set, is streamed (sendfile) after the bytes appended to
// `out` — the zero-copy static-file path.
file_path string
}
ResponseControl is the out-of-band channel for an append-style handler: the handler appends the raw HTTP response bytes (status line + headers + body) directly into the reused out buffer and sets these fields to influence how the reactor treats the connection.
struct Server #
struct Server {
pub:
family net.AddrFamily = .ip6
port int = 3000
max_request_buffer_size int = 8192
timeout_in_seconds int = 30
user_data voidptr
mut:
listen_fds []int = []int{len: max_thread_pool_size, cap: max_thread_pool_size, init: -1}
epoll_fds []int = []int{len: max_thread_pool_size, cap: max_thread_pool_size, init: -1}
threads []thread = []thread{len: max_thread_pool_size, cap: max_thread_pool_size}
request_handler fn (HttpRequest) !HttpResponse = unsafe { nil }
append_handler AppendHandler = unsafe { nil }
make_state fn () voidptr = unsafe { nil }
running &stdatomic.AtomicVal[bool] = stdatomic.new_atomic(false)
shutting_down &stdatomic.AtomicVal[bool] = stdatomic.new_atomic(false)
stopped &stdatomic.AtomicVal[bool] = stdatomic.new_atomic(true)
active_requests &stdatomic.AtomicVal[int] = stdatomic.new_atomic(0)
}
fn (Server) handle #
fn (s &Server) handle() ServerHandle
handle returns a reusable handle for waiting on or shutting down the server.
fn (Server) run #
fn (mut server Server) run() !
run starts the server and begins listening for incoming connections.
struct ServerConfig #
struct ServerConfig {
pub:
family net.AddrFamily = .ip6
port int = 3000
max_request_buffer_size int = 8192
timeout_in_seconds int = 30
// handler is the classic contract: it builds and returns a full HttpResponse.
// Set exactly ONE of `handler` or `append_handler`.
handler fn (HttpRequest) !HttpResponse = unsafe { nil }
user_data voidptr
// make_state, when set, is called ONCE per worker thread at startup; the value
// it returns reaches every request on that worker as HttpRequest.worker_state.
// It is the lock-free per-worker state hook: each worker gets its own instance
// (a DB connection, a reused scratch buffer) with no shared pool and no mutex.
make_state fn () voidptr = unsafe { nil }
// append_handler is the zero-copy contract (see AppendHandler): it appends the
// raw response into the connection's reused write buffer instead of returning
// one. When set, it is used instead of `handler`. Set exactly one of the two.
append_handler AppendHandler = unsafe { nil }
}
ServerConfig bundles the parameters needed to start a fasthttp server.
struct ServerHandle #
struct ServerHandle {
ptr voidptr
}
ServerHandle exposes lifecycle controls for a running fasthttp.Server.
fn (ServerHandle) wait_till_running #
fn (h ServerHandle) wait_till_running(params WaitTillRunningParams) !int
wait_till_running waits until the server transitions to its serving state.
fn (ServerHandle) shutdown #
fn (h ServerHandle) shutdown(params ShutdownParams) !
shutdown gracefully stops accepting new requests and waits for active requests to finish.
struct ShutdownParams #
struct ShutdownParams {
pub:
timeout time.Duration = time.infinite
retry_period_ms int = 10
}
ShutdownParams configures how long graceful shutdown should wait for in-flight requests.
struct Slice #
struct Slice {
pub:
start int
len int
}
struct WaitTillRunningParams #
struct WaitTillRunningParams {
pub:
max_retries int = 100
retry_period_ms int = 10
}
WaitTillRunningParams allows parametrizing the calls to ServerHandle.wait_till_running().
- README
- fn decode_http_request
- fn frame_expected_total
- fn frame_head_len
- fn frame_request_length
- fn frame_request_length_lim
- fn frame_request_length_lim_idx
- fn new_server
- fn parse_http1_request_line
- fn ResponseTakeoverMode.from
- fn Step.from
- type AppendHandler
- enum ResponseTakeoverMode
- enum Step
- struct HttpRequest
- struct HttpResponse
- struct ResponseControl
- struct Server
- struct ServerConfig
- struct ServerHandle
- struct ShutdownParams
- struct Slice
- struct WaitTillRunningParams