How to Implement Rate Limiting Middleware in Gin: 3 Production-Ready Methods

Rate limiting middleware in Gin is implemented as a gin.HandlerFunc that inspects incoming requests, checks against a limiter algorithm, and calls c.AbortWithStatusJSON(429) to stop processing when limits are exceeded, otherwise allowing c.Next() to continue the handler chain.

Implementing rate limiting middleware in Gin requires understanding how the framework orchestrates request processing. The gin-gonic/gin repository uses a HandlersChain architecture where middleware functions execute sequentially, giving you precise control over request flow before your application logic runs.

How Gin Processes Middleware Chains

Gin builds a handler chain (HandlersChain) for every request. When you call engine.Use(...) or group.Use(...), the supplied HandlerFunc instances are appended to the group's Handlers slice in routergroup.go (lines 64-68) and later merged with route-specific handlers via combineHandlers (lines 41-48).

During request execution, the Engine creates a fresh Context, copies the merged HandlersChain into c.handlers, and invokes c.Next() defined in context.go (lines 85-96). The Next method walks the chain sequentially, allowing each middleware to decide whether to continue processing or abort the request entirely.

This architecture means a rate limiting middleware only needs to:

  1. Inspect the request (client IP, path, headers).
  2. Check a limiter (token bucket, fixed window, etc.).
  3. Abort if over limit using c.AbortWithStatusJSON or c.Abort.
  4. Return normally to let c.Next() continue to subsequent handlers.

Building a Custom Token Bucket Rate Limiter

In-Memory Per-IP Implementation

For simple deployments, you can implement a token bucket algorithm using Go's golang.org/x/time/rate package. The middleware extracts the client IP using c.ClientIP() (implemented in context.go lines 970-1024) and maintains a separate limiter per IP address.

package main

import (
	"net/http"
	"sync"
	"time"

	"github.com/gin-gonic/gin"
	"golang.org/x/time/rate"
)

type limiterStore struct {
	mu      sync.Mutex
	clients map[string]*rate.Limiter
	r       rate.Limit
	b       int
}

func newLimiterStore(r rate.Limit, b int) *limiterStore {
	return &limiterStore{
		clients: make(map[string]*rate.Limiter),
		r:       r,
		b:       b,
	}
}

func (s *limiterStore) getLimiter(ip string) *rate.Limiter {
	s.mu.Lock()
	defer s.mu.Unlock()
	lim, exists := s.clients[ip]
	if !exists {
		lim = rate.NewLimiter(s.r, s.b)
		s.clients[ip] = lim
	}
	return lim
}

func RateLimitMiddleware(store *limiterStore) gin.HandlerFunc {
	return func(c *gin.Context) {
		ip := c.ClientIP()
		lim := store.getLimiter(ip)
		if !lim.Allow() {
			c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{
				"error": "rate limit exceeded",
			})
			return
		}
		c.Next()
	}
}

func main() {
	r := gin.Default()
	limStore := newLimiterStore(5, 10) // 5 req/sec, burst 10
	r.Use(RateLimitMiddleware(limStore))
	
	r.GET("/ping", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{"msg": "pong"})
	})
	r.Run()
}

Key implementation details:

  • c.ClientIP() respects Gin's trusted proxy configuration, essential for accurate per-IP limiting behind load balancers.
  • c.AbortWithStatusJSON immediately stops the handler chain and returns HTTP 429, preventing downstream handlers from executing.

Using Production-Ready Libraries

Implementing with ulule/limiter

For distributed systems or advanced features (Redis backends, custom keys), use the github.com/ulule/limiter/v3 package with its dedicated Gin middleware driver.

package main

import (
	"net/http"

	"github.com/gin-gonic/gin"
	"github.com/ulule/limiter/v3"
	memory "github.com/ulule/limiter/v3/drivers/store/memory"
	ginmiddleware "github.com/ulule/limiter/v3/drivers/middleware/gin"
)

func main() {
	rate, _ := limiter.NewRateFromFormatted("10-M") // 10 requests per minute
	store := memory.NewStore()
	limiterInstance := limiter.New(store, rate)
	rateLimiter := ginmiddleware.NewMiddleware(limiterInstance)

	r := gin.Default()
	r.Use(rateLimiter) // Applied globally

	r.GET("/resource", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{"status": "ok"})
	})
	r.Run()
}

The ginmiddleware.NewMiddleware function returns a standard gin.HandlerFunc that integrates seamlessly with Gin's HandlersChain, checking limits before invoking c.Next().

Applying Middleware to Specific Routes

You can restrict rate limiting to specific API sections using Router Groups. In routergroup.go, the Group method creates a new RouterGroup whose Handlers slice is merged with parent middleware via combineHandlers.

api := r.Group("/api")
api.Use(RateLimitMiddleware(limStore)) // Only limits /api/* routes
{
    api.GET("/users", listUsers)
    api.POST("/users", createUser)
}

This approach leverages group.Use (lines 64-68 in routergroup.go) to inject the middleware only into specific sub-trees of your routing hierarchy.

Key Source Files and Implementation Details

Understanding these core files helps debug and extend your rate limiting implementation:

  • gin.go (lines 37-44): Contains the global Use method that adds middleware to the root router's Handlers slice.
  • routergroup.go (lines 41-48): Implements combineHandlers, which merges group-level and route-level handlers into the final execution chain.
  • routergroup.go (lines 64-68): Defines RouterGroup.Use, allowing middleware attachment to specific route groups.
  • context.go (lines 85-96): Contains the Next method that iterates through c.handlers, executing middleware sequentially.
  • context.go (lines 970-1024): Implements ClientIP, which extracts the real client IP while respecting TrustedProxies configuration.

Summary

  • Gin middleware is any function matching func(*gin.Context) that controls execution flow via c.Next() and c.Abort().
  • Rate limiting logic executes before route handlers; call c.AbortWithStatusJSON(429) to reject over-limit requests.
  • Per-IP tracking should use c.ClientIP() from context.go to handle proxy configurations correctly.
  • Global or scoped application is controlled through engine.Use (global) or group.Use (specific routes) defined in routergroup.go.
  • Algorithm choice ranges from simple in-memory token buckets (golang.org/x/time/rate) to distributed solutions (ulule/limiter).

Frequently Asked Questions

How do I accurately extract client IP for per-IP rate limiting in Gin?

Use c.ClientIP() as implemented in context.go (lines 970-1024). This method respects the TrustedProxies and RemoteIPHeaders configuration, ensuring you receive the real client IP even when running behind load balancers or reverse proxies. Avoid using c.Request.RemoteAddr directly, as it may return the proxy's IP rather than the end user's.

Can I apply different rate limits to different API routes?

Yes. Create separate RouterGroup instances using r.Group("/path") and attach distinct middleware via group.Use() as defined in routergroup.go. Each group maintains its own Handlers slice that merges with the global chain, allowing you to apply strict limits to authentication endpoints while keeping public endpoints unrestricted.

What is the difference between c.Abort() and c.Next() in Gin middleware?

c.Next() (context.go lines 85-96) passes control to the next handler in the HandlersChain, continuing execution. c.Abort() (and its variant c.AbortWithStatusJSON) immediately stops the chain by setting c.index to abortIndex, ensuring no subsequent middleware or route handlers execute. For rate limiting, use c.AbortWithStatusJSON(429, ...) to return the proper HTTP status while halting processing.

Which rate limiting algorithm should I use with Gin?

For single-instance applications, use a token bucket via golang.org/x/time/rate for smooth request distribution and burst handling. For distributed systems or persistent storage needs, use github.com/ulule/limiter/v3 with Redis backing. Avoid simple counters for production traffic, as they create thundering herd problems at window boundaries; token buckets provide more consistent traffic shaping.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →