Rate Limiting in Go: Controlling Traffic with Efficiency

Isuru Harischandra
Towards Dev
Published in
5 min readJul 28, 2023

--

Photo by Jon Cellier on Unsplash

Introduction

Rate limiting is a crucial technique in building scalable and resilient systems. It helps to control the flow of traffic by imposing restrictions on the number of requests allowed within a specified time frame. Implementing rate limiting in Go can ensure optimal resource utilization and protect your applications from being overwhelmed by excessive traffic or abusive behavior. In this blog post, we will explore rate limiting techniques in Go and provide practical code examples to help you implement them effectively.

Understanding Rate Limiting

Rate limiting involves defining a set of rules that determine how many requests a client can make in a given time window. It ensures that the system can handle the load and prevents abuse or denial-of-service attacks. The two common approaches to rate limiting are:

  1. Fixed Window Rate Limiting: In this approach, the rate limit is enforced within a fixed time window. For example, if the rate limit is set to 100 requests per minute, the system will allow up to 100 requests in any given 60-second window. Requests exceeding this limit will be rejected or delayed until the next time window.
  2. Token Bucket Rate Limiting: Token bucket rate limiting is based on the concept of tokens being consumed from a bucket. The bucket is initially filled with a fixed number of tokens, and each token represents a request. When a client wants to make a request, it must acquire a token from the bucket. If the bucket is empty, the client must wait until a token becomes available.

Implementing Rate Limiting in Go

Go provides a built-in package called golang.org/x/time/rate that offers rate limiting capabilities. Let's explore how to implement rate limiting using both the fixed window and token bucket approaches.

Fixed Window Rate Limiting

package main

import (
"fmt"
"golang.org/x/time/rate"
"time"
)

func main() {
limiter := rate.NewLimiter(rate.Limit(100), 1) // Allow 100 requests per second

for i := 0; i < 200; i++ {
if !limiter.Allow() {
fmt.Println("Rate limit exceeded. Request rejected.")
continue
}
// Process the request
fmt.Println("Request processed successfully.")
time.Sleep(time.Millisecond * 100) // Simulate request processing time
}
}

In the code snippet above, we create a limiter using rate.NewLimiter with a rate limit of 100 requests per second. The limiter.Allow() method is called for each request, which returns true if the request is allowed or false if the rate limit is exceeded. If the rate limit is exceeded, the request is rejected.

Token Bucket Rate Limiting

package main

import (
"fmt"
"golang.org/x/time/rate"
"time"
)

func main() {
limiter := rate.NewLimiter(rate.Limit(10), 5) // Allow 10 requests per second with a burst of 5

for i := 0; i < 15; i++ {
if err := limiter.Wait(context.TODO()); err != nil {
fmt.Println("Rate limit exceeded. Request rejected.")
continue
}
// Process the request
fmt.Println("Request processed successfully.")
time.Sleep(time.Millisecond * 100) // Simulate request processing time
}
}

In the above code, we create a limiter using rate.NewLimiter with a rate limit of 10 requests per second and a burst of 5. The limiter.Wait() method is called for each request, which blocks until a token becomes available. If the bucket is empty and no tokens are available, the request is rejected.

Dynamic Rate Limiting

Dynamic rate limiting involves adjusting the rate limits based on dynamic factors such as client behavior, system load, or business rules. This technique allows you to adapt the rate limits in real-time to optimize resource utilization and provide a better user experience. Let’s take a look at an example of dynamic rate limiting in Go:

package main

import (
"fmt"
"golang.org/x/time/rate"
"time"
)

func main() {
limiter := rate.NewLimiter(rate.Limit(100), 1) // Initial rate limit of 100 requests per second

// Dynamic rate adjustment
go func() {
time.Sleep(time.Minute) // Adjust rate every minute
limiter.SetLimit(rate.Limit(200)) // Increase rate limit to 200 requests per second
}()

for i := 0; i < 300; i++ {
if !limiter.Allow() {
fmt.Println("Rate limit exceeded. Request rejected.")
continue
}
// Process the request
fmt.Println("Request processed successfully.")
time.Sleep(time.Millisecond * 100) // Simulate request processing time
}
}

In the code snippet above, we create a limiter with an initial rate limit of 100 requests per second. We then start a goroutine that adjusts the rate limit to 200 requests per second after a minute. This allows us to dynamically adapt the rate limit based on changing conditions.

Adaptive Rate Limiting

Adaptive rate limiting dynamically adjusts the rate limits based on the response times or error rates of previous requests. It allows the system to automatically adapt to varying traffic conditions, ensuring optimal performance and resource utilization. Let’s take a look at an example of adaptive rate limiting in Go:

package main

import (
"fmt"
"golang.org/x/time/rate"
"time"
)

func main() {
limiter := rate.NewLimiter(rate.Limit(100), 1) // Initial rate limit of 100 requests per second

// Adaptive rate adjustment
go func() {
for {
responseTime := measureResponseTime() // Measure the response time of previous requests
if responseTime > 500*time.Millisecond {
limiter.SetLimit(rate.Limit(50)) // Decrease rate limit to 50 requests per second
} else {
limiter.SetLimit(rate.Limit(100)) // Increase rate limit to 100 requests per second
}
time.Sleep(time.Minute) // Adjust rate every minute
}
}()

for i := 0; i < 200; i++ {
if !limiter.Allow() {
fmt.Println("Rate limit exceeded. Request rejected.")
continue
}
// Process the request
fmt.Println("Request processed successfully.")
time.Sleep(time.Millisecond * 100) // Simulate request processing time
}
}

func measureResponseTime() time.Duration {
// Measure the response time of previous requests
// Implement your own logic to measure the response time
return time.Millisecond * 200
}

In the above code snippet, we simulate measuring the response time of previous requests using the measureResponseTime function. Based on the measured response time, we dynamically adjust the rate limit by setting different values using limiter.SetLimit. This allows the system to adapt its rate limiting strategy based on the observed response times.

Conclusion

Photo by Jo Jo on Unsplash

Rate limiting is an essential technique for maintaining the stability and security of your Go applications. By effectively controlling the flow of incoming requests, you can prevent resource exhaustion and ensure a fair distribution of resources. In this blog post, we explored the concepts of fixed window and token bucket rate limiting and provided code snippets demonstrating their implementation in Go using the golang.org/x/time/rate package. Incorporate rate limiting into your applications to build resilient systems capable of handling varying levels of traffic with efficiency.

Happy coding!

--

--

COO of Bitzquad | BSc. (Hons) in Information Systems - Reading | Final Year Undergraduate in University of Colombo School of Computing