Skip to content

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: epoll on Linux, kqueue on macOS/BSD, IOCP on Windows. On Linux each worker owns its own SO_REUSEPORT listener (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_state hook 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_path and the body is streamed with sendfile(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 out buffer and returns a Step. 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 value ServerConfig.make_state returned on this worker thread (see below); nil if unset.
  • ctl ResponseControl — out-of-band controls:
    • takeover_mode.manual hands the fd off (you own it, e.g. SSE/WebSocket);.reusable means 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 in out; keep the connection alive.
    • .close — send out, 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 #

@[heap]
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 #

@[params]
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 #

@[params]
struct WaitTillRunningParams {
pub:
	max_retries     int = 100
	retry_period_ms int = 10
}

WaitTillRunningParams allows parametrizing the calls to ServerHandle.wait_till_running().