Logo
Published on

Rate Limiting in Go: Harnessing Gin Framework with Limiter Middleware

Authors
  • Name
    Twitter

In today’s web development landscape, it’s vital to ensure that web services remain responsive even during heavy traffic. One effective strategy to achieve this is through rate limiting. For developers utilizing the Gin framework in Go, the limiter middleware offers an elegant solution for this challenge. This guide will walk you through creating a custom rate control middleware using the Gin framework paired with the limiter middleware.


Complete Middleware Code

Let’s first inspect the complete code:

rate_control.go:

package main

import (
 "encoding/json"
 "fmt"
 "github.com/gin-gonic/gin"
 "github.com/ulule/limiter/v3"
 "github.com/ulule/limiter/v3/drivers/middleware/gin"
 "github.com/ulule/limiter/v3/drivers/store/memory"
 "strings"
 "time"
)

// globalRate defines the default rate limit for routes that don't have a specific configuration.
var globalRate = limiter.Rate{
 Period: 1 * time.Hour,
 Limit:  1000,
}

// routeNameMap maps API routes to route names for configuration lookup.
var routeNameMap = map[string]string{
 "/api/users": "users",
 "/api/items": "items",
}

// rateMapJSON contains predefined rate configurations in JSON format.
var rateMapJSON = `{
 "default:users": "1000-M",
 "strict:users": "10-S",
 "default:items": "1000-M",
 "strict:items": "10-S"
}`

// RateConfig is a mapping of route names to rate limit strings.
type RateConfig map[string]string

// getRateConfigFromDB simulates fetching rate configurations from a database.
func getRateConfigFromDB(mode, routeName string) (string, error) {
 // Replace this with your actual logic to fetch rate configuration from a database.
 // For this example, we'll use the predefined rateMapJSON.
 return rateMapJSON, nil
}

// parseRate converts a rate limit string to a limiter.Rate struct.
func parseRate(rateStr string) (limiter.Rate, error) {
 parts := strings.Split(rateStr, "-")
 if len(parts) != 2 {
  return limiter.Rate{}, fmt.Errorf("invalid rate format: %s", rateStr)
 }

 limit, err := limiter.NewRateFromFormatted(rateStr)
 if err != nil {
  return limiter.Rate{}, err
 }
 return limit, nil
}

// retrieveRateConfig fetches the rate configuration for a route.
func retrieveRateConfig(mode, routeName string) (limiter.Rate, error) {
 rateConfigJSON, err := getRateConfigFromDB(mode, routeName)
 if err != nil {
  // Return an error along with an empty rate limit.
  return limiter.Rate{}, err
 }

 rateConfig := make(RateConfig)
 err = json.Unmarshal([]byte(rateConfigJSON), &rateConfig)
 if err != nil {
  // Return an error along with an empty rate limit.
  return limiter.Rate{}, err
 }

 rateStr, exists := rateConfig[mode+":"+routeNameMap[routeName]]
 if !exists {
  // Return an error along with an empty rate limit.
  return limiter.Rate{}, fmt.Errorf("rate configuration not found for mode: %s, routeName: %s", mode, routeName)
 }

 return parseRate(rateStr)
}

// RateControl is a middleware to handle rate limiting.
func RateControl(c *gin.Context) {
 // Determine the route name dynamically.
 routeName := c.FullPath()

 mode := "default" // Replace this with your actual mode retrieval logic.

 // Retrieve the rate configuration or use a global rate as fallback.
 rate, err := retrieveRateConfig(mode, routeName)
 if err != nil {
  rate = globalRate
 }

 // Create a rate limiter based on the route name and mode.
 storeWithPrefix := memory.NewStoreWithOptions(
  &memory.Options{
   Prefix:   mode + ":" + routeName + ":",
   MaxRetry: 3,
  },
 )
 rateLimiter := limiter.New(storeWithPrefix, rate)

 // Apply the rate limiter middleware.
 limiter_gin.RateLimiter(rateLimiter).Middleware(c)
}

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

 // Use RateControl middleware globally for all routes.
 r.Use(RateControl)

 // Define your routes
 r.GET("/api/users", func(c *gin.Context) {
  c.JSON(200, gin.H{"message": "Users route"})
 })

 r.GET("/api/items", func(c *gin.Context) {
  c.JSON(200, gin.H{"message": "Items route"})
 })

 r.Run(":8080")
}

Deep Dive Into the Code

1. Configuration and Mapping

// globalRate defines the default rate limit for routes that don't have a specific configuration.  
var globalRate = limiter.Rate{  
 Period: 1 * time.Hour,  
 Limit:  1000,  
}  
  
// routeNameMap maps API routes to route names for configuration lookup.  
var routeNameMap = map[string]string{  
 "/api/users": "users",  
 "/api/items": "items",  
}

The globalRate establishes a default rate limit, setting a cap of 1000 requests per hour. The routeNameMap links specific API routes to respective names, streamlining the configuration lookup.

2. Fetching and Parsing Rate Configurations

a. Fetching the Rate Config:

// getRateConfigFromDB simulates fetching rate configurations from a database.  
func getRateConfigFromDB(mode, routeName string) (string, error) {  
 // Replace this with your actual logic to fetch rate configuration from a database.  
 // For this example, we'll use the predefined rateMapJSON.  
 return rateMapJSON, nil  
}

Here, getRateConfigFromDB is a mock function illustrating fetching rate configurations from a database. In an actual application, you'd replace this with your database logic.

b. Parsing Rate Config:

// parseRate converts a rate limit string to a limiter.Rate struct.  
func parseRate(rateStr string) (limiter.Rate, error) {  
 parts := strings.Split(rateStr, "-")  
 if len(parts) != 2 {  
  return limiter.Rate{}, fmt.Errorf("invalid rate format: %s", rateStr)  
 }  
  
 limit, err := limiter.NewRateFromFormatted(rateStr)  
 if err != nil {  
  return limiter.Rate{}, err  
 }  
 return limit, nil  
}

The parseRate function translates rate limit strings into the limiter.Rate structure.

c. Retrieving and Applying Rate Config:

// retrieveRateConfig fetches the rate configuration for a route.  
func retrieveRateConfig(mode, routeName string) (limiter.Rate, error) {  
 rateConfigJSON, err := getRateConfigFromDB(mode, routeName)  
 if err != nil {  
  // Return an error along with an empty rate limit.  
  return limiter.Rate{}, err  
 }  
  
 rateConfig := make(RateConfig)  
 err = json.Unmarshal([]byte(rateConfigJSON), &rateConfig)  
 if err != nil {  
  // Return an error along with an empty rate limit.  
  return limiter.Rate{}, err  
 }  
  
 rateStr, exists := rateConfig[mode+":"+routeNameMap[routeName]]  
 if !exists {  
  // Return an error along with an empty rate limit.  
  return limiter.Rate{}, fmt.Errorf("rate configuration not found for mode: %s, routeName: %s", mode, routeName)  
 }  
  
 return parseRate(rateStr)  
}

retrieveRateConfig combines fetching and parsing, determining the appropriate rate based on the mode and route.

3. RateControl Middleware

/ RateControl is a middleware to handle rate limiting.  
func RateControl(c *gin.Context) {  
 // Determine the route name dynamically.  
 routeName := c.FullPath()  
  
 mode := "default" // Replace this with your actual mode retrieval logic.  
  
 // Retrieve the rate configuration or use a global rate as fallback.  
 rate, err := retrieveRateConfig(mode, routeName)  
 if err != nil {  
  rate = globalRate  
 }  
  
 // Create a rate limiter based on the route name and mode.  
 storeWithPrefix := memory.NewStoreWithOptions(  
  &memory.Options{  
   Prefix:   mode + ":" + routeName + ":",  
   MaxRetry: 3,  
  },  
 )  
 rateLimiter := limiter.New(storeWithPrefix, rate)  
  
 // Apply the rate limiter middleware.  
 limiter_gin.RateLimiter(rateLimiter).Middleware(c)  
}

This middleware handles rate limiting. It dynamically determines the route name, fetches and applies the corresponding rate configuration, and sets up the rate limiter specific to the detected route and mode.

4. Server Initialization and Route Definition

func main() {  
 r := gin.Default()  
  
 // Use RateControl middleware globally for all routes.  
 r.Use(RateControl)  
  
 // Define your routes  
 r.GET("/api/users", func(c *gin.Context) {  
  c.JSON(200, gin.H{"message": "Users route"})  
 })  
  
 r.GET("/api/items", func(c *gin.Context) {  
  c.JSON(200, gin.H{"message": "Items route"})  
 })  
  
 r.Run(":8080")  
}

Our primary function (main) initializes the Gin engine and designates the RateControl middleware as a global middleware. The defined routes, /api/users and /api/items, exemplify the middleware in action.


Conclusion

Rate limiting is a crucial aspect of modern web development to ensure stability and performance. The combined power of Go, the Gin framework, and the limiter middleware makes implementing this feature a more manageable task. The provided middleware can serve as a foundation upon which you can build and adapt according to your application's requirements. Remember, when tailoring for real-world scenarios, consider both performance implications and user experience. Happy coding!