Skip to main content

Gin Rate Limiting

Rate limiting is an essential technique for protecting your web APIs from abuse, denial-of-service attacks, and excessive consumption of resources. In this article, we'll explore how to implement rate limiting in the Gin web framework to control the number of requests a client can make to your API within a specific time frame.

Introduction to Rate Limiting

Rate limiting restricts the number of requests a client can make to your API within a defined time period. For example, you might want to limit clients to 100 requests per minute. When implemented correctly, rate limiting:

  • Protects your API from abuse and attacks
  • Prevents resource exhaustion
  • Ensures fair usage among all clients
  • Reduces costs for pay-per-use infrastructure
  • Improves overall system stability

Let's learn how to implement rate limiting in a Gin application using popular rate limiting libraries.

Basic Rate Limiting with time/rate Package

Go's standard library provides the time/rate package, which implements a "token bucket" rate limiting algorithm. We can use this to create a simple rate limiting middleware for Gin.

First, let's install the necessary packages:

bash
go get -u github.com/gin-gonic/gin

Now, let's create a simple rate limiter middleware using the time/rate package:

go
package main

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

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

// IPRateLimiter stores rate limiters for different IP addresses
type IPRateLimiter struct {
ips map[string]*rate.Limiter
mu *sync.RWMutex
r rate.Limit
b int
}

// NewIPRateLimiter creates a new rate limiter for each IP
func NewIPRateLimiter(r rate.Limit, b int) *IPRateLimiter {
return &IPRateLimiter{
ips: make(map[string]*rate.Limiter),
mu: &sync.RWMutex{},
r: r,
b: b,
}
}

// GetLimiter returns the rate limiter for the provided IP address
func (i *IPRateLimiter) GetLimiter(ip string) *rate.Limiter {
i.mu.RLock()
limiter, exists := i.ips[ip]
i.mu.RUnlock()

if !exists {
i.mu.Lock()
limiter = rate.NewLimiter(i.r, i.b)
i.ips[ip] = limiter
i.mu.Unlock()
}

return limiter
}

func main() {
// Create a rate limiter that allows 5 requests per second with a burst of 10
limiter := NewIPRateLimiter(5, 10)

r := gin.Default()

// Use the rate limiter middleware
r.Use(func(c *gin.Context) {
ip := c.ClientIP()
limiter := limiter.GetLimiter(ip)
if !limiter.Allow() {
c.JSON(http.StatusTooManyRequests, gin.H{
"message": "Too many requests. Please try again later.",
})
c.Abort()
return
}
c.Next()
})

r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})

r.Run(":8080")
}

In this example, we're creating a rate limiter that:

  • Allows 5 requests per second (r=5)
  • Permits a burst of up to 10 requests (b=10)
  • Creates a separate rate limiter for each client IP address

When a client makes too many requests, they'll receive a 429 Too Many Requests HTTP response.

Enhanced Rate Limiting with go-redis/redis_rate

For production applications, you might want a more sophisticated rate limiting solution, especially when running multiple instances of your API. Redis is commonly used for distributed rate limiting.

Let's implement rate limiting using the go-redis/redis_rate library:

bash
go get github.com/go-redis/redis/v8
go get github.com/go-redis/redis_rate/v9

Now, let's implement Redis-based rate limiting:

go
package main

import (
"context"
"net/http"
"time"

"github.com/gin-gonic/gin"
"github.com/go-redis/redis/v8"
"github.com/go-redis/redis_rate/v9"
)

func main() {
// Create a Redis client
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})

// Create a rate limiter
limiter := redis_rate.NewLimiter(rdb)

r := gin.Default()

// Use rate limiting middleware
r.Use(func(c *gin.Context) {
// Get client IP address
ip := c.ClientIP()

// Create a unique key for this client
key := "rate_limit:" + ip

// Allow 10 requests per minute
res, err := limiter.Allow(c.Request.Context(), key, redis_rate.PerMinute(10))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Rate limiting error",
})
c.Abort()
return
}

// Set rate limit headers
c.Header("X-RateLimit-Limit", "10")
c.Header("X-RateLimit-Remaining", string(res.Remaining))
c.Header("X-RateLimit-Reset", string(res.ResetAfter/time.Second))

// If rate limit exceeded, return 429 Too Many Requests
if res.Allowed == 0 {
c.JSON(http.StatusTooManyRequests, gin.H{
"message": "Too many requests. Please try again later.",
})
c.Abort()
return
}

c.Next()
})

r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})

r.Run(":8080")
}

This Redis-based solution:

  • Limits each IP to 10 requests per minute
  • Tracks rate limits across multiple instances of your application
  • Provides rate limit information in HTTP headers
  • Handles rate limit resets automatically

Using gin-contrib/limiter for Gin

The Gin community has created middleware specifically for rate limiting, which may be easier to integrate:

bash
go get github.com/gin-contrib/limiter

Here's how to use it:

go
package main

import (
"time"

"github.com/gin-contrib/limiter"
"github.com/gin-contrib/limiter/memory"
"github.com/gin-gonic/gin"
)

func main() {
r := gin.Default()

// Create a rate limiter that allows 5 requests per minute
store := memory.NewStore()
rate := limiter.Rate{
Period: 1 * time.Minute,
Limit: 5,
}

// Use the rate limiter middleware
r.Use(limiter.New(store, rate))

r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})

r.Run(":8080")
}

This approach is simpler but still effective for basic rate limiting needs.

Advanced Rate Limiting Techniques

For more sophisticated applications, consider these advanced rate limiting approaches:

Different Rate Limits for Different Endpoints

You might want to apply different rate limits to different API endpoints. For example, allowing more requests for public endpoints and fewer for resource-intensive operations:

go
func main() {
r := gin.Default()
limiter := NewIPRateLimiter(5, 10)

// Public endpoint with higher limit
publicGroup := r.Group("/public")
publicGroup.Use(createRateLimiterMiddleware(limiter, 20, 30))

// Admin endpoint with lower limit
adminGroup := r.Group("/admin")
adminGroup.Use(createRateLimiterMiddleware(limiter, 5, 10))

// Your routes here...

r.Run(":8080")
}

func createRateLimiterMiddleware(limiter *IPRateLimiter, r rate.Limit, b int) gin.HandlerFunc {
// Implementation here...
}

Rate Limiting by User ID

For authenticated APIs, you might want to rate limit based on user ID instead of IP address:

go
func RateLimitByUserID() gin.HandlerFunc {
limiter := NewUserRateLimiter(5, 10)

return func(c *gin.Context) {
// Get user ID from context (after authentication)
userID, exists := c.Get("userID")
if !exists {
// Fallback to IP-based limiting if no user ID
userID = c.ClientIP()
}

key := fmt.Sprintf("%v", userID)
limiter := limiter.GetLimiter(key)

if !limiter.Allow() {
c.JSON(http.StatusTooManyRequests, gin.H{
"message": "Rate limit exceeded",
})
c.Abort()
return
}

c.Next()
}
}

Implementing a Sliding Window Algorithm

For more precise rate limiting, you could implement a sliding window algorithm:

go
func SlidingWindowRateLimiter() gin.HandlerFunc {
// Implementation details would go here
// This is more complex and would typically use Redis to track
// timestamps of requests within a sliding window
}

Real-world Application Example

Let's build a more complete API with rate limiting applied differently to various endpoints:

go
package main

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

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

// IPRateLimiter implementation (as shown earlier)...

func main() {
r := gin.Default()

// Create different rate limiters for different endpoint groups
publicLimiter := NewIPRateLimiter(10, 20) // 10 requests/second, burst of 20
authLimiter := NewIPRateLimiter(5, 10) // 5 requests/second, burst of 10
adminLimiter := NewIPRateLimiter(2, 5) // 2 requests/second, burst of 5

// Public endpoints (higher limit)
public := r.Group("/api/v1")
public.Use(createLimiterMiddleware(publicLimiter))
{
public.GET("/products", getProducts)
public.GET("/categories", getCategories)
}

// Authenticated user endpoints (medium limit)
auth := r.Group("/api/v1/user")
auth.Use(createLimiterMiddleware(authLimiter))
{
auth.GET("/profile", getProfile)
auth.PUT("/profile", updateProfile)
auth.POST("/orders", createOrder)
}

// Admin endpoints (lowest limit)
admin := r.Group("/api/v1/admin")
admin.Use(createLimiterMiddleware(adminLimiter))
{
admin.POST("/products", createProduct)
admin.DELETE("/products/:id", deleteProduct)
}

r.Run(":8080")
}

func createLimiterMiddleware(limiter *IPRateLimiter) gin.HandlerFunc {
return func(c *gin.Context) {
ip := c.ClientIP()
ipLimiter := limiter.GetLimiter(ip)

if !ipLimiter.Allow() {
c.JSON(http.StatusTooManyRequests, gin.H{
"error": "Rate limit exceeded",
"retry_after": "Please try again later",
})
c.Abort()
return
}

c.Next()
}
}

// Handler functions would be implemented here...
func getProducts(c *gin.Context) {
// Implementation...
c.JSON(http.StatusOK, gin.H{"products": []string{"Product 1", "Product 2"}})
}

func getCategories(c *gin.Context) {
// Implementation...
c.JSON(http.StatusOK, gin.H{"categories": []string{"Category 1", "Category 2"}})
}

func getProfile(c *gin.Context) {
// Implementation...
c.JSON(http.StatusOK, gin.H{"user": "user1", "email": "[email protected]"})
}

func updateProfile(c *gin.Context) {
// Implementation...
c.JSON(http.StatusOK, gin.H{"status": "profile updated"})
}

func createOrder(c *gin.Context) {
// Implementation...
c.JSON(http.StatusCreated, gin.H{"order_id": "12345", "status": "created"})
}

func createProduct(c *gin.Context) {
// Implementation...
c.JSON(http.StatusCreated, gin.H{"product_id": "789", "status": "created"})
}

func deleteProduct(c *gin.Context) {
// Implementation...
c.JSON(http.StatusOK, gin.H{"status": "product deleted"})
}

This example shows a typical API structure where different endpoints have different rate limits based on their sensitivity and resource requirements.

Testing Rate Limiting

You can test your rate limiting implementation using tools like curl or hey:

bash
# Install hey
go get -u github.com/rakyll/hey

# Test rate limiting
hey -n 100 -c 10 http://localhost:8080/ping

When rate limiting is working correctly, you should see some requests succeed and others fail with a 429 status code once the rate limit is exceeded.

Summary

Rate limiting is a crucial technique for protecting your API and ensuring fair usage. In this article, we've explored:

  1. Why rate limiting is important
  2. How to implement basic rate limiting with Go's time/rate package
  3. Advanced rate limiting with Redis for distributed applications
  4. How to use the Gin-specific rate limiting middleware
  5. Advanced techniques like per-endpoint rate limiting and user-based rate limiting
  6. A complete example of a real-world API with different rate limits for different endpoints

By implementing rate limiting in your Gin applications, you can protect your services from abuse, ensure fair usage, and maintain better stability under high load.

Additional Resources

Exercises

  1. Modify the basic rate limiter to expire IP entries after a period of inactivity
  2. Implement a tiered rate limiting system based on user subscription levels (e.g., free, premium, enterprise)
  3. Add detailed logging of rate limit events to help diagnose API usage patterns
  4. Create a dashboard endpoint that shows current rate limit status for all active users/IPs
  5. Implement rate limiting with different time windows (per second, per minute, per hour, per day)


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)