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 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_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.

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 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 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 #

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

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

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

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

@[typedef]

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 #

@[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().