Benchmarking High-Performance Go Web Frameworks with an Empty HTTP Handler
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