Post

Overhead of web frameworks

Some benchmarks of Go's standard library against popular frameworks.

I’ve been using Go for a while now and I had this question in the beginning - should I use the standard library or pick a framework for web servers? So I decided to stop guessing and actually benchmark these things properly.

This isn’t going to be one of those posts where I tell you that it depends and leave you hanging. Well I hope not. We’ll see real numbers from my machine, and I’ll try to explain what they actually mean. Also will try to add code snippets wherever possible.

One important thing I wanna highlight is that this benchmark only measures raw routing overhead. Like how fast the framework can accept a request and send a response. Real-world performance depends on a lot more: JSON serialization, database queries, middleware chains (logging, auth, CORS), number of routes, connection pooling, etc. Take these numbers as a comparison of framework overhead, not as a predictor of a Go app’s actual performance.

Go vs Node The pic is generated from Gemini Nano Banana.

The contenders

Here’s what we’re comparing:

  • net/http — Go’s standard library. No external dependencies.
  • Fiber — Built on top of fasthttp. Claims to be the fastest.
  • Gin — Probably the most popular Go web framework.
  • Echo — Another popular choice, known for being minimal.
  • Bonus: Fastify — Fast and low overhead web framework, for Node.js.
  • Bonus: Express — Fast and low overhead web framework, for Node.js.

Let’s do some analysis.

My hardware

I use a Macbook Pro.

Model M4 Pro
CPU 14 Cores
GPU 20 Cores
Neural Engine 16 Cores
RAM 48 GB

For benchmarking HTTP, I’m using wrk!

If you are on mac and happen to use brew

  • you can install it using brew install wrk
  • here’s the GitHub repo for wrk

This is the wrk command that will compile us the result for a given code snippet:

1
wrk -t12 -c400 -d30s http://localhost:3000/ping

That’s 12 threads, 400 concurrent connections, running for 30 seconds. Decently fine kind of load where differences actually show up.

Setup

Each server does the exact same thing:

  • return “pong” when we hit /ping
  • no JSON parsing, no middleware, no database calls, just raw throughput :)

Standard Library (net/http)

1
2
3
4
5
6
7
8
9
10
11
12
package main

import (
    "net/http"
)

func main() {
    http.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("pong"))
    })
    http.ListenAndServe(":3000", nil)
}

Nothing fancy. This is what your code looks like when you refuse to import any 3rd party lib :P

Fiber

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import (
    "log"
    "github.com/gofiber/fiber/v3"
)

func main() {
    app := fiber.New()
    app.Get("/ping", func(c fiber.Ctx) error {
        return c.SendString("pong!")
    })
    log.Fatal(app.Listen(":3000"))
}

Fiber’s syntax reminds me of Express.js lol.

Gin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import (
    "github.com/gin-gonic/gin"
)

func main() {
    gin.SetMode(gin.ReleaseMode)
    r := gin.New()
    r.GET("/ping", func(c *gin.Context) {
        c.String(200, "pong")
    })
    r.Run(":3000")
}

Note: I’m using gin.New() instead of gin.Default() to skip the default logger and recovery middleware. Fairer comparison that way.

Echo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import (
    "net/http"
    "github.com/labstack/echo/v4"
)

func main() {
    e := echo.New()
    e.HideBanner = true
    e.GET("/ping", func(c echo.Context) error {
        return c.String(http.StatusOK, "pong")
    })
    e.Start(":3000")
}

Echo feels a lot like Gin. The API is slightly different but the vibe is the same.

Results!!!!

Framework Requests/sec Avg Latency Transfer/sec
Fiber 163,150 2.23ms 18.83 MB
Gin 90,474 4.34ms 10.35 MB
net/http 90,157 4.35ms 10.32 MB
Echo 89,452 4.38ms 10.24 MB

Fiber is genuinely fast

Fiber sitting at the top with ~163k req/sec — almost 1.8x faster than the rest. It is built on fasthttp, which takes a completely different approach than net/http. The fasthttp library reuses request/response objects aggressively and avoids allocations wherever possible. That matters a lot under heavy load.

But Fiber’s speed comes with a tradeoff. It’s not compatible with the standard net/http interfaces. All those nice middleware packages built for standard Go? Yeah, they won’t work. You’re locked into Fiber’s ecosystem.

Gin, net/http, and Echo are basically tied

This is the interesting part. Gin, net/http, and Echo all hover around 90k req/sec. The difference between them is honestly just noise. And we’re talking about ~1% variance.

Gin slightly edging out net/http was unexpected tbh. Shows that a well-optimized router does not necessarily add overhead. Gin’s radix tree routing is pretty efficient.

The gap exists

Fiber is clearly in a different league here. If raw throughput is your main concern and you’re okay with the fasthttp ecosystem, Fiber is the obvious choice.

But for net/http-based frameworks (Gin, Echo), you’re not losing anything significant. The overhead is basically zero. In exchange, you get:

  • a cleaner routing syntax
  • built-in parameter extraction
  • middleware chains
  • request binding and validation

For most apps, that tradeoff is worth it. Atleast I would like to think so.

Memory stats?

Raw throughput is only part of the story. I also watched memory usage during the benchmarks:

Framework Baseline Under Load Stable After Load
net/http ~8 MB ~45 MB ~15 MB
Fiber ~10 MB ~55 MB ~18 MB
Gin ~12 MB ~52 MB ~20 MB
Echo ~11 MB ~50 MB ~19 MB

All of these are completely reasonable. The numbers spike during the benchmark (because goroutines, connection buffers, etc.) and then settle back down. Go’s garbage collector does its job.

Fiber uses slightly more memory because fasthttp maintains its own connection pools. Not a big deal unless you’re running on a very constrained system.

Some observations

The framework rarely matters for performance.

Imo, in a real application, your request handler isn’t just returning “pong” of course, rather it is:

  • parsing JSON
  • validating input
  • calling a database
  • maybe calling other services
  • serializing a response

Any of these operations will dwarf the overhead of your framework. A single database query is 1-10ms. And the framework’s overhead, maybe 10 microseconds.

And that’s a 100x-1000x difference.

If your service is slow, it’s almost never because of the framework but maybe:

  1. slow database queries
  2. missing indexes
  3. n+1 query problems — here’s a nice goprisma article on that
  4. network calls that didn’t get cached
  5. JSON serialization of massive payloads … well that’s a separate topic altogther.

So what should we use imo?

Choosing a framework usually comes down to whether you prioritize extreme speed or ease of use. If you’re building a standard REST API and want to get things done quickly, Gin or Echo are the smartest choices. They offer a great balance of productivity and solid performance.

Go with Fiber only if you’re hitting massive scale and need every ounce of throughput (or if you’re coming from an Express.js background and want that familiar feel). But if you’re a purist who wants zero extra baggage or you’re building a simple library, sticking with the standard net/http is never a bad move.

Bonus: Node.js comparison :P

Since we’re here, I was curious how Node.js stacks up. Actually even the idea of writing an article about how Go’s web frameworks stack up against Node.js’s web frameworks was born when I was comparing how Go and Nodejs web frameworks stack up against each other. So I thought why not write a blog post about it? And here we are. I tested expressjs (the classic) and Fastify (the fast one).

Express:

1
2
3
4
5
6
7
8
const express = require('express');
const app = express();

app.get('/ping', (req, res) => {
  res.send('pong');
});

app.listen(3000);

Fastify:

1
2
3
4
5
6
7
const fastify = require('fastify')({ logger: false });

fastify.get('/ping', async (request, reply) => {
  return 'pong';
});

fastify.listen({ port: 3000 });

Results:

Framework Requests/sec Avg Latency Transfer/sec
Fiber 163,150 2.23ms 18.83 MB
Fastify (nodejs) 103,895 4.36ms 16.65 MB
Gin 90,474 4.34ms 10.35 MB
net/http 90,157 4.35ms 10.32 MB
Echo 89,452 4.38ms 10.24 MB
Express (nodejs) 67,676 7.27ms 14.84 MB

I didn’t expect this. Fastify actually beats Gin, net/http, and Echo. Node’s V8 engine is pretty well optimized it appears. But yeah not so surprising when fastify is literally advertising itself as “Fast and low overhead web framework, for Node.js”.

Express is the slowest here, which makes sense — it’s designed for developer experience, not raw speed. But 67k req/sec is still plenty fast for most apps.

Fiber remains king though!


I am not really a number person to be honest (very relevant from the fact that I ride a Royal Enfield lmao and drive a Honda). I just had some time to spare and I wanted to see if there’s a huge difference or not.

This post is licensed under CC BY 4.0 by the author.