Card image

Rate Limiter Part 3 - Example with Golang

golangbackendrate limitersystem design

In the previous two sections, I have outlined the concept and application of Rate Limiter, which you can review here:

  • Part 1 - Concept and Application
  • Part 2 - Commonly Used Algorithms In today's tutorial, Part 3, I will explain how to implement a Rate Limiter in a real-world project.

I. Prerequisite

In this article, I will be using:

  • Backend: Golang with GIN and the Rate Limiter middleware library: gin-rate-limiter
  • Client: NodeJs along with a proxy server

II. Objective

For a practical example, I need to address some requirements as follows:

  • Limit the number of requests coming from IP Addresses, assuming the request quota for each IP Address is the same.
  • The number of requests for each IP Address is limited to 5 requests per second.

These are simple requirements. Let's get started!

notion image

III. Setup project

I won't cover the basics of Go and Gin again; we'll start with a pre-configured Golang HTTP Server using GIN:

package main

import (
	"time"

	"github.com/gin-gonic/gin"
	"github.com/khaaleoo/gin-rate-limiter/core"
)

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

	r.GET("/me", func(c *gin.Context) {
		c.String(200, "me")
	})

	r.Run(":3000")
}

Install library gin-rate-limiter:

go get github.com/khaaleoo/gin-rate-limiter

Setup Complete, Now Let’s Address Each Use Case Above 💪🏻

IV. Use case 1 - IP Limiter

The gin-rate-limiter works as a middleware for GIN. We initialize an instance of the middleware as follows:

rateLimiterOption := ratelimiter.RateLimiterOption{
		Limit: 1,
		Burst: 2,
		Len:   1 * time.Second,
}

// Create an IP rate limiter instance
rateLimiterMiddleware := ratelimiter.RequireRateLimiter(ratelimiter.RateLimiter{
    RateLimiterType: ratelimiter.IPRateLimiter,
    Key:             "iplimiter_maximum_requests_for_ip_test",
    Option: rateLimiterOption,
})

There are a few parameters declared above, which I can explain as follows:

  • 🔥 Burst: The limit on the number of tokens that an actor (requester) can consume at once.
  • ⌛️ Len: The interval after which the bucket is cleaned/reset. If a client does not make a request within this period, everything is reset to the initial state.
  • 🔁 Limit: The number of tokens that are refreshed every second.
  • 🐧 RateLimiterType: The type of rate limiter middleware; here, we choose ratelimiter.IPRateLimiter.

Apply middleware to the /me route:

r.GET("/me", rateLimiterMiddleware, func(c *gin.Context) {
   c.String(200, "me")
})

Here is the full code snippet for the HTTP server side:

package main

import (
	"time"

	"github.com/gin-gonic/gin"
	"github.com/khaaleoo/gin-rate-limiter/core"
)

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

	rateLimiterOption := ratelimiter.RateLimiterOption{
		Limit: 1,
		Burst: 2,
		Len:   1 * time.Second,
	}

	rateLimiterMiddleware := ratelimiter.RequireRateLimiter(ratelimiter.RateLimiter{
		RateLimiterType: ratelimiter.IPRateLimiter,
		Key:             "iplimiter_maximum_requests_for_ip_test",
		Option: rateLimiterOption,
	})

	// Apply rate limiter middleware to a route
	r.GET("/limited-route", rateLimiterMiddleware, func(c *gin.Context) {
		c.String(200, "me")
	})

	r.Run(":3000")
}

That's awesome. Now I will simulate clients making HTTP requests to the initialized server.

I won't delve into setting up the client side deeply, just simply create a basic Node.js application and a proxy server, with the following concept:

Simulate 2 clients (A and B) making requests to the server. All requests will be asynchronous, and there will be successCount and errorCount variables to count the number of successful and failed requests.

(async () => {
  try {
    console.time("concatenation");
    console.log("🏇 Process start...");
    await Promise.all([
      ...Array(2)
        .fill(0)
        .map((_, i) => GetMe(i)), // from Client A
      ...Array(3)
        .fill(0)
        .map((_, i) => GetMeWithDifferentIPAddress(i)), // from Client B
    ]);
  } catch (err) {
    console.log({ err });
  } finally {
    console.log("✅ Process end...");
    console.timeEnd("concatenation");
    console.log({ successCount, errorCount });
  }
})();

Please go ahead and run the application, and you will see the results:

notion image

Based on the results, we can see the following:

  • The request execution time is 61.677ms (within the <1s window length config).
  • The backend server allows only 1 client to make a maximum of 2 requests per second. Therefore, a total of 4 requests from the local IP and the Proxy Server IP were successful, while the 3rd request from the Proxy Server IP failed due to exceeding the allowed quota.

We have successfully addressed the requirements of Use Case 1.

V. Use Case 2 - Adjusting Token Quantity

To adjust the token quantity, we simply need to modify the parameters of the gin-rate-limiter middleware instance.

rateLimiterOption := ratelimiter.RateLimiterOption{
    Limit: 1,
    Burst: 5, // maximum 5 requests
    Len:   1 * time.Second,
}

VI. Use Case 3 - Implementing for a Group of IPs/Routes

We can apply middleware to a group of routes for specific IPs as follows:

rateLimiterOption := ratelimiter.RateLimiterOption{
		Limit: 1,
		Burst: 2,
		Len:   1 * time.Second,
	}

rateLimiterMiddleware := ratelimiter.RequireRateLimiter(ratelimiter.RateLimiter{
    RateLimiterType: ratelimiter.IPRateLimiter,
    Key:             "user_group",
    Option: rateLimiterOption,
})
    
user := router.Group("/user")
user.Use(rateLimiterMiddleware)
{
    user.GET("/me",
        userAPIService.GetMeHdl(),
    )
    
    user.PUT("/me",
        userAPIService.UpdateProfileHdl(),
    )
}

Alternatively, apply different rate limiters to different routes by declaring multiple instances.

Important: These instances are distinguished by the RateLimiterType and Key pair.

rateLimiterGetOption := ratelimiter.RateLimiterOption{
    Limit: 1,
    Burst: 10,
    Len:   1 * time.Second,
}

rateLimiterUpdateOption := ratelimiter.RateLimiterOption{
    Limit: 1,
    Burst: 5,
    Len:   1 * time.Second,
}


getProfileMiddleware := ratelimiter.RequireRateLimiter(ratelimiter.RateLimiter{
    RateLimiterType: ratelimiter.IPRateLimiter,
    Key:             "get_profile",
    Option: rateLimiterGetOption,
})

updateProfileMiddleware := ratelimiter.RequireRateLimiter(ratelimiter.RateLimiter{
    RateLimiterType: ratelimiter.IPRateLimiter,
    Key:             "update_profile",
    Option: rateLimiterUpdateOption,
})
    
user := router.Group("/user")
{
    user.GET("/me",
        getProfileMiddleware,
        userAPIService.GetMeHdl(),
    )
    
    user.PUT("/me",
        updateProfileMiddleware,
        userAPIService.UpdateProfileHdl(),
    )
}

VII. Conclusion

Great job! You have successfully applied the Rate Limiter to a specific application with the simplest example. Thank you for reading the article.

VIII. References