Fading Coder

An Old Coder’s Final Dance

Home > Tech > Content

Benchmarking High-Performance Go Web Frameworks with an Empty HTTP Handler

Tech 1

This benchmark compares several popular Go web frameworks using a minimal HTTP endpoint that returns an empty body. The goal is to focus on scheduling and request handling overhead rather than protocol parsing or payload processing. For reference, a custom C++ network server is included as a baseline.

Key metrics:

  • Throughput (bandwidth) and especially QPS as proxies for the framework’s scheduling efficiency
  • Requests use HTTP/1.1 with persistent connections (wrk’s default) and the response body is empty to minimize protocol overhead

A custom C++ implementation is used for comparison. Its architectural traits include:

  • Portable socket multiplexing (poll, epoll, kqueue, port, select, IOCP)
  • Lock-free algorithms where applicable
  • Thread pool execution
  • Socket connection pooling
  • Multi-level task queues

Note:

  • This setup primarily exposes the scheduling performance of the server-side socket layer and request router
  • Keep-Alive is enabled implicitly via HTTP/1.1 in wrk

Test Environment

  • System setting: ulimit -n 2000
  • Load generator: wrk
  • Due to resource constraints, the wrk cliant and the server under test run on the same machine

C++ Baseline Server

  • Startup command (2 worker threads, HTTP on port 8080, up to 2000 concurrent connections):

    • ./proxy_server -i2000 -o2000 -w2 -x8080
  • HTTP response sample:

    $ curl -i http://localhost:8080/
    HTTP/1.1 200 OK
    Content-Length: 0
    Connection: keep-alive
    
  • wrk result:

    $ wrk -d 100s -c 1024 -t 8 http://localhost:8080/
    Running 2m test @ http://localhost:8080/
      8 threads and 1024 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency    13.03ms    3.80ms 100.73ms   86.97%
        Req/Sec     9.43k     1.64k   39.35k    88.23%
      7509655 requests in 1.67m, 444.03MB read
      Socket errors: connect 0, read 794, write 2, timeout 0
    Requests/sec:  75018.11
    Transfer/sec:      4.44MB
    

Go: go-restful

  • main_go-restful.go

    package main
    
    import (
        "net/http"
        restful "github.com/emicklei/go-restful"
    )
    
    func main() {
        // Use an explicit container instead of the default for clarity
        container := restful.NewContainer()
    
        api := new(restful.WebService)
        api.Route(api.GET("/").To(handleEmpty))
        container.Add(api)
    
        _ = http.ListenAndServe(":8080", container)
    }
    
    func handleEmpty(req *restful.Request, resp *restful.Response) {
        // Write an empty payload; defaults to 200 OK
        _, _ = resp.Write([]byte{})
    }
    
  • HTTP response sample:

    $ curl -i http://localhost:8080/
    HTTP/1.1 200 OK
    Date: Mon, 21 Oct 2019 03:54:27 GMT
    Content-Length: 0
    
  • wrk result:

    $ wrk -d 100s -c 1024 -t 8 http://localhost:8080/
    Running 2m test @ http://localhost:8080/
      8 threads and 1024 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency    19.72ms   10.57ms 331.94ms   87.67%
        Req/Sec     6.52k     1.24k   23.75k    80.42%
      5180908 requests in 1.67m, 370.57MB read
      Socket errors: connect 0, read 844, write 3, timeout 0
    Requests/sec:  51757.61
    Transfer/sec:      3.70MB
    

Go: Echo

  • main_go-echo.go

    package main
    
    import (
        "net/http"
    
        "github.com/labstack/echo"
    )
    
    func main() {
        srv := echo.New()
    
        // Respond with no body using NoContent for minimal overhead
        srv.GET("/", func(c echo.Context) error {
            return c.NoContent(http.StatusOK)
        })
    
        srv.Logger.Fatal(srv.Start(":8080"))
    }
    
  • HTTP response sample:

    $ curl -i http://localhost:8080/
    HTTP/1.1 200 OK
    Content-Type: text/plain; charset=UTF-8
    Date: Mon, 21 Oct 2019 04:09:24 GMT
    Content-Length: 0
    
  • wrk result:

    $ wrk -d 100s -c 1024 -t 8 http://localhost:8080/
    Running 2m test @ http://localhost:8080/
      8 threads and 1024 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency    17.32ms    8.19ms 252.60ms   90.70%
        Req/Sec     7.52k     1.35k   39.96k    80.55%
      5974370 requests in 1.67m, 660.92MB read
      Socket errors: connect 0, read 431, write 67, timeout 0
    Requests/sec:  59686.09
    Transfer/sec:      6.60MB
    

Go: Iris

  • main_go-iris.go

    package main
    
    import (
        "time"
    
        "github.com/kataras/iris"
        "github.com/kataras/iris/cache"
    )
    
    func main() {
        app := iris.New()
        app.Logger().SetLevel("error")
    
        // Keep the cache middleware as in the original setup
        app.Get("/", cache.Handler(10*time.Second), func(ctx iris.Context) {
            ctx.Markdown([]byte{})
        })
    
        _ = app.Run(iris.Addr(":8080"))
    }
    
  • HTTP response sample:

    $ curl -i http://localhost:8080/
    HTTP/1.1 200 OK
    Content-Type: text/html; charset=UTF-8
    Date: Mon, 21 Oct 2019 04:11:59 GMT
    Content-Length: 0
    
  • wrk result:

    $ wrk -d 100s -c 1024 -t 8 http://localhost:8080/
    Running 2m test @ http://localhost:8080/
      8 threads and 1024 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency    22.03ms    7.99ms 140.47ms   84.58%
        Req/Sec     5.79k   775.23    19.31k    80.35%
      4608572 requests in 1.67m, 505.43MB read
      Socket errors: connect 0, read 726, write 22, timeout 0
    Requests/sec:  46041.23
    Transfer/sec:      5.05MB
    

Go: net/http + httprouter

  • main_go-httprouter.go

    package main
    
    import (
        "io"
        "log"
        "net/http"
    
        "github.com/julienschmidt/httprouter"
    )
    
    func main() {
        r := httprouter.New()
        r.GET("/", func(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) {
            _, _ = io.WriteString(w, "")
        })
    
        log.Fatal(http.ListenAndServe(":8080", r))
    }
    
  • HTTP response sample:

    $ curl -i http://localhost:8080/
    HTTP/1.1 200 OK
    Date: Mon, 21 Oct 2019 04:15:33 GMT
    Content-Length: 0
    
  • wrk result:

    $ wrk -d 100s -c 1024 -t 8 http://localhost:8080/
    Running 2m test @ http://localhost:8080/
      8 threads and 1024 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency    16.71ms    7.72ms 268.45ms   87.79%
        Req/Sec     7.71k     1.58k   21.27k    82.12%
      6130281 requests in 1.67m, 438.47MB read
      Socket errors: connect 0, read 693, write 36, timeout 0
    Requests/sec:  61243.74
    Transfer/sec:      4.38MB
    

Go: Chi

  • main_go-chi.go

    package main
    
    import (
        "net/http"
    
        "github.com/go-chi/chi"
    )
    
    func main() {
        mux := chi.NewRouter()
        mux.Get("/", func(w http.ResponseWriter, _ *http.Request) {
            _, _ = w.Write([]byte{})
        })
    
        _ = http.ListenAndServe(":8080", mux)
    }
    
  • HTTP response sample:

    $ curl -i http://localhost:8080/
    HTTP/1.1 200 OK
    Date: Mon, 21 Oct 2019 04:18:42 GMT
    Content-Length: 0
    
  • wrk result:

    $ wrk -d 100s -c 1024 -t 8 http://localhost:8080/
    Running 2m test @ http://localhost:8080/
      8 threads and 1024 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency    17.17ms    8.47ms 253.47ms   90.07%
        Req/Sec     7.65k     1.42k   26.08k    79.76%
      6071695 requests in 1.67m, 434.28MB read
      Socket errors: connect 0, read 110, write 2, timeout 0
    Requests/sec:  60658.49
    Transfer/sec:      4.34MB
    

Results Overview

| Runtime/Framework | CPU idle | Memory (RSS) | Requests/sec | | --- | --- | --- | --- | | C++ baseline | 15%–20% | 6 MB | 75018.11 | | Go httprouter | 0%–1.5% | 28 MB | 61243.74 | | Go Chi | 0%–1% | 28 MB | 60658.49 | | Go Echo | 0%–0.5% | 28 MB | 59686.09 | | Go go-restful | 0%–0.5% | 34 MB | 51757.61 | | Go Iris | 0%–1% | 37 MB | 46041.23 |

Observations:

  • Among the Go options, httprouter, Chi, and Echo cluster together at the top; httprouter edges out slightly in this run
  • The custom C++ server retains a performance lead in this micro-benchmark, though the difference is not overwhelming
  • Go implementations consume more memory in these configurations, while the C++ process is notably lean
  • Go’s developer ergonomics are excellent, though dependency management can introduce churn with upgrades
Tags: golang

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.