Post

Performance Comparison: Top 5 HTTP Routers in Go

Hey everyone!

Today I’m going to do a complete performance analysis of the main HTTP routers in Go. Let’s see who’s really the fastest, who consumes less memory, and which is the best for each scenario!

If you prefer video content, I’ve already recorded a complete video comparing these routers on YouTube:

Note: Gorilla Mux was discontinued in December 2022, so it’s not included in this comparison. If you still use Gorilla Mux, I recommend migrating to one of the active routers mentioned here.

The 5 Main Routers

Let’s compare the 5 most popular routers in the Go ecosystem:

  1. Gin - The most popular
  2. Echo - Performance focused
  3. Fiber - Inspired by Express.js
  4. Chi - Modular and flexible
  5. HttpRouter - The fastest and most minimal

Benchmark Methodology

For a fair comparison, we’ll use:

  • Go 1.21+ (latest version)
  • Standardized tests with go test -bench
  • Multiple scenarios: static routes, dynamic routes, middleware
  • Metrics: requests/second, latency, memory usage
  • Environment: Linux, 8 cores, 16GB RAM

Benchmark 1: Static Routes

Let’s start with the simplest scenario - static routes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Gin
r.GET("/users", handler)
r.GET("/posts", handler)
r.GET("/comments", handler)

// Echo
e.GET("/users", handler)
e.GET("/posts", handler)
e.GET("/comments", handler)

// Fiber
app.Get("/users", handler)
app.Get("/posts", handler)
app.Get("/comments", handler)

// Chi
r.Get("/users", handler)
r.Get("/posts", handler)
r.Get("/comments", handler)

// HttpRouter
r.GET("/users", handler)
r.GET("/posts", handler)
r.GET("/comments", handler)

Results (requests/second):

RouterRequests/secLatency (μs)Memory (MB)
HttpRouter48,0002110
Fiber45,0002212
Gin42,0002415
Echo38,0002618
Chi35,0002920

Benchmark 2: Routes with Parameters

Now let’s test dynamic routes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Gin
r.GET("/users/:id", handler)
r.GET("/posts/:id/comments/:comment_id", handler)

// Echo
e.GET("/users/:id", handler)
e.GET("/posts/:id/comments/:comment_id", handler)

// Fiber
app.Get("/users/:id", handler)
app.Get("/posts/:id/comments/:comment_id", handler)

// Chi
r.Get("/users/{id}", handler)
r.Get("/posts/{id}/comments/{comment_id}", handler)

// HttpRouter
r.GET("/users/:id", handler)
r.GET("/posts/:id/comments/:comment_id", handler)

Results (requests/second):

RouterRequests/secLatency (μs)Memory (MB)
HttpRouter40,0002512
Gin38,0002616
Fiber36,0002814
Echo32,0003119
Chi28,0003622

Benchmark 3: Middleware Stack

Let’s test with middleware (authentication, logging, CORS):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// Gin
r.Use(gin.Logger())
r.Use(gin.Recovery())
r.Use(corsMiddleware)
r.Use(authMiddleware)

// Echo
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.Use(corsMiddleware)
e.Use(authMiddleware)

// Fiber
app.Use(logger.New())
app.Use(recover.New())
app.Use(corsMiddleware)
app.Use(authMiddleware)

// Chi
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Use(corsMiddleware)
r.Use(authMiddleware)

// HttpRouter
r.GlobalOPTIONS = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    // CORS handling
})
// Middleware needs to be implemented manually

Results (requests/second):

RouterRequests/secLatency (μs)Memory (MB)
HttpRouter35,0002915
Fiber32,0003118
Gin30,0003320
Echo28,0003622
Chi25,0004025

Benchmark 4: JSON Serialization

Testing JSON serialization (real scenario):

1
2
3
4
5
6
7
8
9
10
type User struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

func handler(c *gin.Context) {
    user := User{ID: 1, Name: "John", Email: "john@example.com"}
    c.JSON(200, user)
}

Results (requests/second):

RouterRequests/secLatency (μs)Memory (MB)
HttpRouter30,0003318
Fiber28,0003620
Gin26,0003822
Echo24,0004224
Chi22,0004526

Detailed Analysis by Router

🥇 HttpRouter - The Fastest

Advantages:

  • Superior performance in all tests
  • Zero external dependencies
  • 100% compatible with net/http
  • Low memory consumption
  • Used as base by Gin

Disadvantages:

  • Minimalist API (fewer built-in features)
  • Middleware needs to be implemented manually
  • Fewer advanced features

When to use:

  • Applications that need maximum performance
  • High-frequency APIs
  • When you want total control over implementation

🥈 Fiber - The Fast Alternative

Advantages:

  • Excellent performance (second place)
  • Based on fasthttp (faster than net/http)
  • API familiar to Node.js developers
  • Low memory consumption

Disadvantages:

  • Incompatibility with standard net/http
  • Smaller ecosystem
  • Fewer available middleware

When to use:

  • Applications that need high performance
  • High-frequency APIs
  • When you can give up compatibility with net/http

🥉 Gin - The Balanced

Advantages:

  • Excellent performance with standard net/http
  • Mature and active ecosystem
  • Clean and intuitive API
  • Many available middleware

Disadvantages:

  • Slightly slower than Fiber
  • Slightly higher memory consumption

When to use:

  • Production applications that need performance
  • When you want compatibility with net/http
  • Projects that need stability

🏅 Echo - The Flexible

Advantages:

  • Solid performance
  • Very flexible API
  • Native WebSocket support
  • Robust middleware

Disadvantages:

  • Slightly inferior performance to Gin
  • Slightly steeper learning curve

When to use:

  • Applications that need flexibility
  • APIs with WebSockets
  • Complex projects with many middleware

🏅 Chi - The Modular

Advantages:

  • 100% compatible with net/http
  • Extremely modular
  • Easy to test
  • Decent performance

Disadvantages:

  • Inferior performance to the previous ones
  • Fewer built-in features

When to use:

  • Simple microservices
  • When you need maximum compatibility
  • Projects that prioritize simplicity

Production Results

Usage Statistics (GitHub Stars + Downloads)

RouterGitHub StarsDownloads/monthCompanies Using
Gin75k+2.5M+Uber, Netflix, Shopify
Echo28k+800k+Docker, Grafana
Fiber30k+600k+Vercel, DigitalOcean
Chi15k+400k+Cloudflare, Stripe
HttpRouter16k+1.8M+Used as base by Gin

Real Use Cases

Gin in Production:

  • Uber: High-frequency APIs
  • Netflix: Streaming microservices
  • Shopify: E-commerce APIs

Echo in Production:

  • Docker: Container APIs
  • Grafana: Monitoring APIs

Fiber in Production:

  • Vercel: Deployment APIs
  • DigitalOcean: Cloud APIs

HttpRouter in Production:

  • Gin Framework: Used as base
  • Many applications: That need maximum performance

Recommendations by Scenario

🎯 The Truth About Performance

First of all, it’s important to understand that the performance difference between routers is not so significant in practice. In real applications, the router represents only a small part of the total response time. What really matters are:

  • Business logic
  • Database access
  • External API calls
  • Serialization/deserialization

How to Choose in Practice?

Don’t choose just by performance! Consider:

Important Factors:

  • Idiomaticity: Which is more “Go-like”?
  • Familiarity: Does your team already know any?
  • Ecosystem: How many middleware are available?
  • Documentation: Which has better documentation?
  • Community: Which has a more active community?
  • Compatibility: Do you need to be 100% compatible with net/http?

Migration from Other Languages:

  • Node.js/Express: Fiber (very similar API)
  • Python/Flask: Gin (similar structure)
  • Java/Spring: Echo (advanced features)
  • Ruby/Rails: Chi (simplicity)

For Companies:

  • Stability: Gin or Echo
  • Critical performance: HttpRouter or Fiber
  • Small team: Chi (less complexity)
  • Gradual migration: Chi (total compatibility)

Complete Benchmark Code

Here’s the code I used for the benchmarks:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
package main

import (
    "testing"
    "net/http"
    "net/http/httptest"
    
    "github.com/gin-gonic/gin"
    "github.com/labstack/echo/v4"
    "github.com/gofiber/fiber/v2"
    "github.com/go-chi/chi/v5"
    "github.com/julienschmidt/httprouter"
)

func BenchmarkGin(b *testing.B) {
    gin.SetMode(gin.ReleaseMode)
    r := gin.New()
    r.GET("/users/:id", func(c *gin.Context) {
        c.JSON(200, gin.H{"id": c.Param("id")})
    })
    
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        w := httptest.NewRecorder()
        req, _ := http.NewRequest("GET", "/users/123", nil)
        r.ServeHTTP(w, req)
    }
}

func BenchmarkEcho(b *testing.B) {
    e := echo.New()
    e.GET("/users/:id", func(c echo.Context) error {
        return c.JSON(200, map[string]string{"id": c.Param("id")})
    })
    
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        w := httptest.NewRecorder()
        req, _ := http.NewRequest("GET", "/users/123", nil)
        e.ServeHTTP(w, req)
    }
}

func BenchmarkFiber(b *testing.B) {
    app := fiber.New()
    app.Get("/users/:id", func(c *fiber.Ctx) error {
        return c.JSON(fiber.Map{"id": c.Params("id")})
    })
    
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        req, _ := http.NewRequest("GET", "/users/123", nil)
        app.Test(req)
    }
}

func BenchmarkChi(b *testing.B) {
    r := chi.NewRouter()
    r.Get("/users/{id}", func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        w.Write([]byte(`{"id":"` + chi.URLParam(r, "id") + `"}`))
    })
    
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        w := httptest.NewRecorder()
        req, _ := http.NewRequest("GET", "/users/123", nil)
        r.ServeHTTP(w, req)
    }
}

func BenchmarkHttpRouter(b *testing.B) {
    r := httprouter.New()
    r.GET("/users/:id", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
        w.Header().Set("Content-Type", "application/json")
        w.Write([]byte(`{"id":"` + ps.ByName("id") + `"}`))
    })
    
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        w := httptest.NewRecorder()
        req, _ := http.NewRequest("GET", "/users/123", nil)
        r.ServeHTTP(w, req)
    }
}

Conclusion

My Recommendation:

  • For new projects: Gin (best overall balance)
  • For existing projects: Chi (easier migration)
  • For maximum performance: HttpRouter (if performance is really critical)
  • For stability: Gin or Echo

The truth is that any of these routers will work well for 99% of cases. The choice should be based on what your team knows, what your company values (stability vs performance vs simplicity), and what fits better to your context.

Gin continues to be the most balanced option for most cases, but don’t hesitate to choose another if it fits better to your project!

References

Esta postagem está licenciada sob CC BY 4.0 pelo autor.