Why fasthttp Outperforms net/http: Inside Go’s Ultra‑Fast Server
This article compares Go’s standard net/http library with the high‑performance fasthttp server, detailing their request‑handling workflows, explaining fasthttp’s optimizations such as connection reuse, sync.Pool and unsafe.Pointer conversions, and walking through core code structures like workerPool, workerChan, and the Serve routine.
Wow, the performance is astonishing. In this episode we explore the underlying implementation of the fasthttp server and discover why it achieves such high speed and which excellent features are worth learning.
Server‑Side Processing Flow Comparison
Before diving into fasthttp’s source code, we first review and compare how the two libraries handle requests, then analyze fasthttp in depth.
net/http Processing Flow
The standard net/http flow is described in detail in a previous article ("Golang Standard Library net/http Implementation – Server Side"). The overall process is illustrated below:
Register routes and their handlers in a map for later lookup.
Start listening for connections; each new connection spawns a goroutine.
Inside the goroutine, continuously read request data and match the URL to a handler via the map.
Execute the matched handler.
net/http creates a new goroutine for every connection, which can become a bottleneck under very high concurrency.
Every incoming connection instantiates a connection object – who can bear that?
fasthttp Processing Flow
The fasthttp request handling process is as follows:
Start listening.
Loop to accept connections and build a worker pool.
Attempt to obtain a connection (net.Conn); first try the ready queue, then the object pool.
Send the obtained net.Conn to a workerChan channel.
Launch a goroutine that continuously reads from the workerChan channel.
Process the request once the connection is retrieved.
The workerChan is a connection‑handling object that contains a channel used to pass connections; each workerChan has a background goroutine that repeatedly fetches connections from this channel for processing.
workerChan is stored in the workerPool temporary object.
Why fasthttp Is Fast
fasthttp’s performance gains stem from several key optimizations:
Connection reuse – workerChan objects are recycled from a ready slice; if none are available, a new one is created from a pool (default capacity 256 × 1024).
Extensive use of sync.Pool for memory reuse – about 30 pools are employed for context, request, header, response, etc.
Use of unsafe.Pointer to convert between []byte and string without additional allocations or copies.
Understanding these features helps explain fasthttp’s speed and how they are applied during request handling.
Underlying Implementation
Simple Example
import (
"github.com/buaazp/fasthttprouter"
"github.com/valyala/fasthttp"
"log"
)
func main() {
// create router
r := fasthttprouter.New()
r.GET("/", Index)
if err := fasthttp.ListenAndServe(":8083", r.Handler); err != nil {
log.Fatalf("ListenAndServe fatal: %s", err)
}
}
func Index(ctx *fasthttp.RequestCtx) {
ctx.WriteString("hello xiaou code!")
}This minimal example starts a service with just a few lines of code.
It creates routes, associates each route with a handler, and then calls ListenAndServe to start listening for requests.
workerPool Structure
The workerPool object represents a pool of workers that handle connections, allowing control over post‑accept processing instead of spawning a goroutine per request as net/http does. Its ready field stores idle workerChan objects, while workerChanPool manages a sync.Pool of workerChan instances.
type workerPool struct {
// handler for matched requests
WorkerFunc ServeHandler
// maximum concurrent workers
MaxWorkersCount int
LogAllErrors bool
// maximum idle worker duration
MaxIdleWorkerDuration time.Duration
Logger Logger
// mutex
lock sync.Mutex
// current worker count
workersCount int
mustStop bool
// idle workers
ready []*workerChan
// stop channel
stopCh chan struct{}
// object pool for workerChan
workerChanPool sync.Pool
connState func(net.Conn, ConnState)
}WorkerFunc is crucial because it is set to Server.serveConn. ready holds idle workerChan objects, and workerChanPool reduces memory allocations by reusing workerChan instances.
Starting the Service
ListenAndServe is the entry point for starting the server. Its internal flow is illustrated below:
The Server.Serve method provides service for connections until the maximum limit (256 × 1024) is reached, at which point it reports an error.
func (s *Server) Serve(ln net.Listener) error {
maxWorkersCount := s.getConcurrency()
s.mu.Lock()
s.ln = append(s.ln, ln)
if s.done == nil {
s.done = make(chan struct{})
}
if s.concurrencyCh == nil {
s.concurrencyCh = make(chan struct{}, maxWorkersCount)
}
s.mu.Unlock()
wp := &workerPool{
WorkerFunc: s.serveConn,
MaxWorkersCount: maxWorkersCount,
LogAllErrors: s.LogAllErrors,
MaxIdleWorkerDuration: s.MaxIdleWorkerDuration,
Logger: s.logger(),
connState: s.setState,
}
wp.Start()
atomic.AddInt32(&s.open, 1)
defer atomic.AddInt32(&s.open, -1)
for {
c, err := acceptConn(s, ln, &lastPerIPErrorTime)
if err != nil {
return err
}
s.setState(c, StateNew)
atomic.AddInt32(&s.open, 1)
if !wp.Serve(c) {
// max workers reached
}
c = nil
}
}Server.Serve performs three main actions:
Initialize and start the worker pool.
Continuously accept connections from the net.Listener.
Dispatch each accepted connection to a workerChan for processing.
If the number of connections exceeds the default limit (256 × 1024), an error is returned.
Starting the Goroutine Pool
After initializing the workerPool, Start launches a cleanup goroutine that periodically removes idle workerChan objects (every 10 seconds).
🚩 The cleanup rule uses a binary‑search algorithm to find the index of the earliest worker that can be cleaned.
func (wp *workerPool) Start() {
if wp.stopCh != nil {
return
}
wp.stopCh = make(chan struct{})
stopCh := wp.stopCh
wp.workerChanPool.New = func() interface{} {
return &workerChan{ch: make(chan net.Conn, workerChanCap)}
}
go func() {
var scratch []*workerChan
for {
wp.clean(&scratch)
select {
case <-stopCh:
return
default:
time.Sleep(wp.getMaxIdleWorkerDuration())
}
}
}()
}The cleanup goroutine prevents wasteful goroutine accumulation during traffic spikes.
Accepting Connections
The acceptConn function uses net.Listener.Accept to obtain connections, similar to net/http.
func acceptConn(s *Server, ln net.Listener, lastPerIPErrorTime *time.Time) (net.Conn, error) {
for {
c, err := ln.Accept()
if err != nil {
// error handling
}
if s.MaxConnsPerIP > 0 {
pic := wrapPerIPConn(s, c)
if pic == nil {
continue
}
c = pic
}
return c, nil
}
}Getting a workerChan
workerPool.getChretrieves an idle workerChan from the ready slice or creates a new one from the pool, then starts a goroutine to handle its channel.
func (wp *workerPool) getCh() *workerChan {
var ch *workerChan
createWorker := false
wp.lock.Lock()
ready := wp.ready
n := len(ready) - 1
if n < 0 {
if wp.workersCount < wp.MaxWorkersCount {
createWorker = true
wp.workersCount++
}
} else {
ch = ready[n]
ready[n] = nil
wp.ready = ready[:n]
}
wp.lock.Unlock()
if ch == nil {
if !createWorker {
return nil
}
vch := wp.workerChanPool.Get()
ch = vch.(*workerChan)
go func() {
wp.workerFunc(ch)
wp.workerChanPool.Put(vch)
}()
}
return ch
}The getCh method works as follows:
First try to obtain a workerChan from the ready queue.
If none are available, create a new workerChan via the object pool.
Launch a goroutine to process data from the channel.
The ready slice operates as a FILO stack, always popping from the end.
Processing a Connection
func (wp *workerPool) workerFunc(ch *workerChan) {
var c net.Conn
var err error
for c = range ch.ch {
if c == nil {
break
}
if err = wp.WorkerFunc(c); err != nil && err != errHijacked {
// error handling
}
if !wp.release(ch) {
break
}
}
wp.lock.Lock()
wp.workersCount--
wp.lock.Unlock()
}The execution flow:
Iterate over the workerChan channel to fetch connections.
For each connection, invoke WorkerFunc (which is Server.serveConn) to handle the request.
After processing, return the workerChan to the ready queue.
🚩 WorkerFunc is actually Server’s serveConn method.
During Server.Serve initialization, Server.serveConn is assigned to workerPool.WorkerFunc.
func (s *Server) ServeConn(c net.Conn) error {
// ...
err := s.serveConn(c)
// ...
}The serveConn routine extracts request parameters, finds the appropriate handler, processes the request, and writes the response back to the client. Internally it also reuses context, request, and response objects via sync.Pool.
Summary
fasthttp and net/http differ significantly in implementation. fasthttp achieves high speed through extensive use of sync.Pool for object reuse and unsafe.Pointer for zero‑copy []byte‑to‑string conversions, among other optimizations. If your application requires high QPS and consistently low latency, fasthttp is a solid choice, though net/http offers broader compatibility for most scenarios.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Su San Talks Tech
Su San, former staff at several leading tech companies, is a top creator on Juejin and a premium creator on CSDN, and runs the free coding practice site www.susan.net.cn.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
