Gin
~80k ★Martini-like web framework, radix tree router
Best-in-class speed for teams happy with a custom context type. Large ecosystem. The go-to default for many Go teams.
func(c *gin.Context) Framework Comparison
Gin, Echo, Fiber, Chi, and Plumego — what each one trades away and what it gets in return. This page is a decision aid, not a benchmark race.
All five toolkits are built for Go HTTP services. The differences are philosophy and scope, not raw performance at typical request volumes. Read the verdict row first, then follow the deep-dive that matches your team's actual constraint.
Star counts are approximate GitHub figures captured in May 2026 and will grow over time. They are a rough ecosystem-size signal, not a quality ranking.
Martini-like web framework, radix tree router
Best-in-class speed for teams happy with a custom context type. Large ecosystem. The go-to default for many Go teams.
func(c *gin.Context) High performance, minimalist web framework
Clean API, good middleware ecosystem, error return in handler is ergonomic. Solid choice when the custom context trade-off is acceptable.
func(c echo.Context) error Express-inspired, Fasthttp-based web framework
Highest raw throughput by benchmark. The tradeoff is total incompatibility with net/http — no stdlib middleware, no net/http handler, no standard library ecosystem.
func(c *fiber.Ctx) error Lightweight, idiomatic router for Go HTTP services
The minimalist's choice. Pure stdlib, zero opinions on response format, service shape, or module stability. If you want to build everything yourself, Chi is the right starting point.
func(w http.ResponseWriter, r *http.Request) Explicit HTTP toolkit — stdlib-only, machine-readable specs, v1.0.0
For teams where route ownership, review clarity, and a shared service shape matter more than framework convenience. The only choice if your team uses AI coding tools and wants machine-readable ownership signals.
func(w http.ResponseWriter, r *http.Request) Read across a row to compare one property across all five toolkits. The matrix does not capture ecosystem size, community, or raw performance — those depend on your benchmark. It captures structural trade-offs that don't change with load.
| Property | Gin | Echo | Fiber | Chi | Plumego |
|---|---|---|---|---|---|
| stdlib handler signature func(w http.ResponseWriter, r *http.Request) | |||||
| net/http middleware reuse No adapter needed for existing stdlib middleware | |||||
| Zero external dependencies Stable core uses only Go standard library | |||||
| Built-in response contract Typed JSON envelope for success and error responses | |||||
| Canonical service shape Reference app for bootstrap, routing, and handler layout | |||||
| Explicit module stability tiers stable / beta / experimental labels with adoption guidance | |||||
| AI / agent-ready control plane specs/task-routing.yaml classifies work before code is opened; specs/change-recipes/ defines ordered steps for 15 change types; module.yaml scopes each package; internal/checks/ enforces boundaries in CI |
Fiber's net/http incompatibility is not a missing feature — it's a deliberate architectural choice that enables higher throughput. See the Fiber deep-dive for when this trade is worth making.
Custom context types (Gin, Echo, Fiber) break compatibility with the existing net/http middleware ecosystem. If you adopt one of these frameworks, every stdlib middleware needs an adapter. With Chi and Plumego, the entire net/http world works out of the box.
// gin.Context — net/http middleware needs adapter
r.GET("/users/:id", func(c *gin.Context) {
id := c.Param("id")
c.JSON(http.StatusOK, gin.H{"id": id})
})
// to reuse net/http middleware:
r.Use(gin.WrapH(myStdlibMiddleware)) // echo.Context + error return
e.GET("/users/:id", func(c echo.Context) error {
id := c.Param("id")
return c.JSON(http.StatusOK,
map[string]any{"id": id})
}) // fasthttp — incompatible with net/http entirely
// zero net/http middleware can be reused
app.Get("/users/:id", func(c *fiber.Ctx) error {
id := c.Params("id")
return c.JSON(fiber.Map{"id": id})
}) // pure stdlib — but no response contract
r.Get("/users/{id}", func(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
// build your own JSON response every time
json.NewEncoder(w).Encode(map[string]any{"id": id})
}) // stdlib signature + typed response contract
r.Get("/users/:id", func(w http.ResponseWriter, r *http.Request) {
id := router.Param(r, "id")
contract.WriteResponse(w, r, http.StatusOK, user, nil)
// → {"data": {...}, "meta": null}
})
// all net/http middleware works without adapters
r.Use(middleware.RequestID)
r.Use(myOtelMiddleware) // any net/http middleware
Gin, Echo, Fiber, and Chi all leave the JSON response format to you. This means each team,
each handler, or each project may produce subtly different shapes. Plumego's
contract module enforces one envelope for both success and error responses.
Gin / Echo / Fiber / Chi — roll your own
// success — each team decides the shape
c.JSON(200, gin.H{
"data": user,
"success": true, // some teams add this
"code": 0, // some teams add this
})
// error — even more inconsistent
c.JSON(400, gin.H{
"error": "name required",
"message": "...", // duplicate? missing?
"code": 1001,
}) Plumego — one contract, all handlers
// success — always {"data": ..., "meta": ...}
contract.WriteResponse(w, r, 200, user, nil)
// error — always {"error": {"type": ..., "message": ...}}
contract.WriteError(w, r,
contract.NewErrorBuilder().
Type(contract.TypeRequired).
Field("name").
Message("name is required").
Build())
// same envelope from every handler, every service The table below is not intended to declare a winner. Each row answers two questions: what does this framework excel at, and what should make you pause. The honest answer to "when NOT to choose Plumego" is at the bottom.
This section is required by Plumego's own content rules. An honest comparison includes cases where the framework is the wrong choice — not just where it wins.
convention-first teams
Plumego keeps route registration, middleware order, and dependency wiring explicitly in your code. If your team prefers a framework that manages structure invisibly, the explicit model adds friction without adding value. Gin or Echo will be more productive.
existing large codebase
Migrating a mature Gin or Echo service to Plumego requires rewriting all handlers from custom context to net/http signatures. If your service is working and the team is productive, that cost is rarely worth it. Stay on what you have.
maximum throughput
Plumego uses a net/http-compatible trie router. Its overhead is minimal but non-zero compared to Fiber's Fasthttp baseline. If you have measured evidence that HTTP routing is your bottleneck (not database, not business logic), Fiber is the right conversation.
long production track record required
Plumego has a v1.0.0 tag and the 9 stable roots have a compatibility promise, but the project has not yet accumulated the long-term production track record that Gin or Echo have. If "proven at scale for 5+ years" is a hard requirement, Gin or Echo is the safer organizational choice.
Benchmark comparisons between these frameworks are widely published. Read them with care.
fiber caveat
Fasthttp avoids standard library allocations by reusing buffers. This yields measurable throughput gains in synthetic benchmarks. However, those gains disappear if you need net/http middleware, use standard library HTTP clients against the same service, or run workloads where the bottleneck is database I/O rather than HTTP parsing.
plumego baseline
The trie router has no reflection and no global state. Sequential benchmarks show Plumego at 4,291 ns/op
versus Chi's 4,547 ns/op on GET /users/:id. Gin and Echo achieve lower sequential numbers
(~3,750 ns/op) but their parallel throughput advantage disappears when the bottleneck shifts to I/O.
See the benchmark table below for the full picture.
general advice
In most Go HTTP services, the bottleneck is database round-trips, external API calls, or serialization — not the router. Framework selection on performance grounds alone is rarely justified without measurements on your specific workload at your target scale.
The decision to switch frameworks is rarely just about API ergonomics — it also involves handler rewrites, middleware replacements, and testing infrastructure. This table gives an honest estimate of the effort involved when migrating an existing service to Plumego.
Migration cost assumes a working service with existing handlers and middleware.
A greenfield service starting from reference/standard-service has no migration cost.
Sequential and parallel dispatch numbers measured with go test -bench -benchmem -count=3.
Route: GET /users/:id. No middleware. linux/amd64, Intel Xeon @ 2.10 GHz.
Includes full httptest request/response cycle. Relative column vs Plumego (1.0×).
Router overhead is rarely the real bottleneck — a database call adds 500 µs–5 ms; the router adds 1–5 µs. Choose based on what matters to your team.
http.ServeMux + PathValue())Overhead comes from per-request context injection (480 B, 5 allocs), parameter map construction, and middleware chain wiring. For a handler spending 1 ms on I/O, router cost is under 0.1% of total request time.
| Scenario | stdlib ns/op | Plumego ns/op | Ratio |
|---|---|---|---|
| Static route | 110 | 367 | 3.3× |
| Single :param + read | 186 | 807 | 4.3× |
| Four :param deep path | 722 | 1036 | 1.4× |
| 70-route mixed table | 247 | 660 | 2.7× |
The four-param deep-path scenario (1.4×) shows overhead converges as path complexity increases — the trie traversal amortises over more work per request.
Reproduce: go test -bench=BenchmarkRouter -benchmem -count=3 ./reference/benchmark/...
| Framework | ns/op | allocs/op | B/op | vs Plumego |
|---|---|---|---|---|
| Plumego this | 4291 | 16 | 5715 | 1.0× |
| Chi | 4547 | 17 | 6036 | 1.06× |
| Gin faster | 3773 | 13 | 5331 | 0.88× |
| Echo faster | 3743 | 13 | 5331 | 0.87× |
| Framework | ns/op | allocs/op | B/op | vs Plumego |
|---|---|---|---|---|
| Plumego this | 663 | 7 | 592 | 1.0× |
| Chi | 1013 | 8 | 912 | 1.53× |
| Gin faster | 285 | 4 | 208 | 0.43× |
| Echo faster | 231 | 4 | 208 | 0.35× |
Among stdlib-compatible frameworks (Chi and Plumego), Plumego is 35% faster under concurrent load (663 vs 1,013 ns/op).
Gin and Echo are ~2.5–3× faster than Plumego in this test because sync.Pool context eliminates per-request allocation —
at the cost of a custom context type incompatible with stdlib middleware.
If stdlib compatibility matters to your team, the relevant comparison is Chi, not Gin or Echo.
Reproduce: cd reference/benchmark && go test -bench=. -benchmem -count=3 ./...
If Plumego matches your team's model, start from the reference app. If another toolkit is the better fit, that's the right call — this page exists to help you decide, not to sell.
Not sure it fits? Read the honest trade-off assessment →