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:
go get -u github.com/gin-gonic/gin
Now, let's create a simple rate limiter middleware using the time/rate
package:
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:
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:
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:
go get github.com/gin-contrib/limiter
Here's how to use it:
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:
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:
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:
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:
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
:
# 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:
- Why rate limiting is important
- How to implement basic rate limiting with Go's
time/rate
package - Advanced rate limiting with Redis for distributed applications
- How to use the Gin-specific rate limiting middleware
- Advanced techniques like per-endpoint rate limiting and user-based rate limiting
- 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
- Gin Framework Documentation
- time/rate Package Documentation
- Redis Rate Limiting Patterns
- Rate Limiting Algorithms
Exercises
- Modify the basic rate limiter to expire IP entries after a period of inactivity
- Implement a tiered rate limiting system based on user subscription levels (e.g., free, premium, enterprise)
- Add detailed logging of rate limit events to help diagnose API usage patterns
- Create a dashboard endpoint that shows current rate limit status for all active users/IPs
- 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! :)