Rate Limiting in Golang: Understanding Fixed and Sliding Windows Algorithm

Imagine you're at an amusement park waiting for a ride. Only a certain number of people are allowed to board each hour. If the line gets too long, anyone arriving has to wait until the next hour before they can board. This idea of restricting access is much like rate limiting in programming, where we control the number of requests a system can handle to avoid overload.

In Golang, we often implement rate limiting with algorithms like the Fixed Window and Sliding Window. Let’s explore these, with examples to help you build your rate limiter.

Fixed Window Rate Limiting

The Fixed Window algorithm allows a set number of requests within a defined period (e.g., 100 requests per minute). If the limit is reached within that period, any additional requests are denied until the next period starts.

How it Works

Think of a fixed window as a "bucket" that resets every minute. If your limit is 100 requests per minute, then you have a fresh bucket every minute to count requests. Once you hit 100, the bucket overflows and additional requests are blocked until the next minute.

Fixed Window Code Example

Here’s a simple example of a fixed window rate limiter in Golang. We'll use a map to track requests by minute:

 package main

import (
    "fmt"
    "sync"
    "time"
)

type RateLimiter struct {
    requests     int
    maxRequests  int
    resetTime    time.Time
    lock         sync.Mutex
}

func NewRateLimiter(maxRequests int, interval time.Duration) *RateLimiter {
    return &RateLimiter{
        maxRequests: maxRequests,
        resetTime:   time.Now().Add(interval),
    }
}

func (rl *RateLimiter) Allow() bool {
    rl.lock.Lock()
    defer rl.lock.Unlock()

    // Reset the count when the interval has passed
    if time.Now().After(rl.resetTime) {
        rl.requests = 0
        rl.resetTime = time.Now().Add(time.Minute)
    }

    // Check if the request can be allowed
    if rl.requests < rl.maxRequests {
        rl.requests++
        return true
    }

    return false
}

func main() {
    limiter := NewRateLimiter(5, time.Minute)

    for i := 0; i < 10; i++ {
        if limiter.Allow() {
            fmt.Println("Request", i+1, "allowed")
        } else {
            fmt.Println("Request", i+1, "denied - rate limit reached")
        }
        time.Sleep(10 * time.Second)
    }
}

Explanation:

  • This example allows 5 requests per minute.

  • Each time Allow() is called, it checks if the current request count exceeds the limit.

  • After a minute, the counter resets.

Sliding Window Rate Limiting

The Sliding Window algorithm is a little more flexible. Instead of counting requests in strict chunks of time, it allows requests in a "moving window" (e.g., the last 60 seconds).

How it Works

Imagine a conveyor belt where items move continuously. As new requests are added to one end, old requests fall off the other end after a certain time. This prevents a "rush" at the start of each minute and provides smoother request handling.

Sliding Window Code Example

To create a sliding window in Golang, we can track the timestamps of each request, ensuring only those within the last 60 seconds are counted.

package main

import (
    "fmt"
    "sync"
    "time"
)

type SlidingWindowRateLimiter struct {
    timestamps  []time.Time
    maxRequests int
    interval    time.Duration
    lock        sync.Mutex
}

func NewSlidingWindowRateLimiter(maxRequests int, interval time.Duration) *SlidingWindowRateLimiter {
    return &SlidingWindowRateLimiter{
        maxRequests: maxRequests,
        interval:    interval,
    }
}

func (swl *SlidingWindowRateLimiter) Allow() bool {
    swl.lock.Lock()
    defer swl.lock.Unlock()

    now := time.Now()
    // Remove timestamps older than the interval
    validTimestamps := []time.Time{}
    for _, ts := range swl.timestamps {
        if now.Sub(ts) < swl.interval {
            validTimestamps = append(validTimestamps, ts)
        }
    }
    swl.timestamps = validTimestamps

    // Allow request if we have room within the window
    if len(swl.timestamps) < swl.maxRequests {
        swl.timestamps = append(swl.timestamps, now)
        return true
    }

    return false
}

func main() {
    limiter := NewSlidingWindowRateLimiter(5, time.Minute)

    for i := 0; i < 10; i++ {
        if limiter.Allow() {
            fmt.Println("Request", i+1, "allowed")
        } else {
            fmt.Println("Request", i+1, "denied - rate limit reached")
        }
        time.Sleep(10 * time.Second)
    }
}

Explanation:

  • This example allows up to 5 requests per minute, but with a sliding window.

  • Each request's timestamp is stored in an array, and only those within the last minute are kept.

  • When a new request comes in, we discard any timestamps older than the interval. If there’s room within the count, the request is allowed.

Comparing Fixed and Sliding Windows

  • Fixed Window: Easier to implement and more predictable, but can lead to bursts at the start of each interval.

  • Sliding Window: Smoother request handling since it moves continuously, though a bit more complex to implement.

Conclusion

Using rate limiting, we can control the flow of requests to keep our systems stable. The fixed window is simple and effective, while the sliding window offers more flexibility.

10
Subscribe to my newsletter

Read articles from Oluwajuwon Falore directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Oluwajuwon Falore
Oluwajuwon Falore

I am a full-Stack (backend leaning) software developer. Experienced with all stages of the development cycle for dynamic web projects. Well-versed in programming languages including HTML5, CSS, JAVASCRIPT, NODEJS, GOLANG, REACTJS, PYTHON, ANGULAR and IONIC.