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 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 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.
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 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 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
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 #
Available on Linux, macOS, Windows, FreeBSD, OpenBSD, NetBSD and DragonFly BSD only.
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
from returns the ResponseTakeoverMode value of an integer or of a field name, or an error if there is none.
fn Step.from #
fn Step.from[W](input W) !Step
from returns the Step value of an integer or of a field name, or an error if there is none.
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 C.sigset_t #
Available on Linux only.
struct C.sigset_t {}
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 #
Available on macOS, FreeBSD, OpenBSD, NetBSD and DragonFly BSD only.
struct Server {
pub mut:
family net.AddrFamily = .ip6
host string
port int
max_request_buffer_size int = 8192
max_request_body_size int = default_max_request_body_size
timeout_in_seconds int = 30
socket_fd int = -1
poll_fd int = -1 // kqueue fd
user_data voidptr
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)
mut:
poll_fds []int = []int{len: bsd_thread_pool_size, cap: bsd_thread_pool_size, init: -1}
threads []thread = []thread{len: bsd_thread_pool_size, cap: bsd_thread_pool_size}
}
Available on Linux only.
@[heap]
struct Server {
pub:
family net.AddrFamily = .ip6
host string
port int = 3000
max_request_buffer_size int = 8192
max_request_body_size int = default_max_request_body_size
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)
}
Available on Windows only.
@[heap]
struct Server {
pub:
family net.AddrFamily = .ip6
host string
port int = 3000
max_request_buffer_size int = 8192
max_request_body_size int = default_max_request_body_size
timeout_in_seconds int = 30
user_data voidptr
mut:
listen_fd C.SOCKET = iocp_invalid_socket
iocp voidptr
threads []thread = []thread{len: iocp_thread_count, cap: iocp_thread_count}
registry &IocpConnRegistry = &IocpConnRegistry{}
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 #
Available on Linux, macOS, Windows, FreeBSD, OpenBSD, NetBSD and DragonFly BSD only.
fn (mut s Server) run() !
run starts the server and enters the main event loop (Kqueue version).
struct ServerConfig #
struct ServerConfig {
pub:
family net.AddrFamily = .ip6
// host is the interface address the listening socket binds to. Empty (the
// default) keeps the historical wildcard bind — 0.0.0.0 for .ip and the
// dual-stack :: for .ip6 — reachable on every interface. Set it to a
// numeric address or host name (e.g. '127.0.0.1') to bind only that
// interface. See resolve_bind_addr for the resolution rules.
host string
port int = 3000
max_request_buffer_size int = 8192
// max_request_body_size independently bounds buffered request payload bytes.
// Set it to 0 only for trusted deployments that intentionally allow unlimited
// in-memory request bodies.
max_request_body_size int = default_max_request_body_size
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 C.sigset_t
- struct HttpRequest
- struct HttpResponse
- struct ResponseControl
- struct Server
- struct ServerConfig
- struct ServerHandle
- struct ShutdownParams
- struct Slice
- struct WaitTillRunningParams