Notes: Token Bucket Rate Limiter in Go

Notes: Token Bucket Rate Limiter in Go

Implementation of Token Bucket Rate Limiter in Go

ยท

3 min read

Table of contents

No heading

No headings in the article.

Token Bucket is one of the most used Rate Limiting algorithms. In Token Bucket, as the name suggests, we have a fixed amount of tokens to be exhausted in a timeframe. However, in our implementation timeframe-based filling is not included, rather we have added RefillTokenBucket method which makes it more flexible.

TokenBucket manages a state with availableTokens and last refill time, lastUpdateTime.

type TokenBucket struct {
    availableTokens int `json:"availableTokens"`
    lastUpdateTime  int `json:"lastUpdateTime"`
}

Now, we have four methods implemented in TokenBucket.

  • Reset: This resets TokenBucket refilling tokens with MAX_TOKEN_CAPACITY.
  • RefillTokenBucket: This refills tokens based on difference between lastUpdateTime and current time.
  • ReduceTokensFromBucket: This reduces tokens by the passed integer amount. In case, there are not enough tokens, it will throw an error.
  • GetTokenBucketState: This just return current TokenBucket.

And this ends the implementation of our Token Bucket. Now to use this, we can add it as middleware for our webservers and use available methods as per our requirements. We can run a goroutine just to refill the bucket after a certain time by calling RefillTokenBucket. We might want to use a lock as well in case of concurrent reads and writes. We'll implement these in upcoming blogs.

In the meantime, this is the complete code with implementation which you can use to understand Token Bucket:

package main

import (
    "errors"
    "fmt"
    "time"
)

const MAX_TOKEN_CAPACITY = 10
const REFILL_RATE = 2 // 2 tokens per second

type TokenBucket struct {
    availableTokens int `json:"availableTokens"`
    lastUpdateTime  int `json:"lastUpdateTime"` // just whenever we refill
}

type ITokenBucket interface {
    Reset() *TokenBucket
    RefillTokenBucket() *TokenBucket
    ReduceTokensFromBucket(tokenCount int) *TokenBucket
    GetTokenBucketState() *TokenBucket
}

func (b *TokenBucket) Reset() *TokenBucket {
    b.availableTokens = MAX_TOKEN_CAPACITY
    b.lastUpdateTime = currentUnixTime()

    return b
}

func (b *TokenBucket) RefillTokenBucket() *TokenBucket {
    // round to nearest integer
    // not required in this case
    tokenRefillCount := (currentUnixTime() - b.lastUpdateTime) * REFILL_RATE
    netTokenCount := b.availableTokens + tokenRefillCount

    b.availableTokens = min(netTokenCount, MAX_TOKEN_CAPACITY)
    b.lastUpdateTime = min(currentUnixTime(), b.lastUpdateTime+(tokenRefillCount/REFILL_RATE))

    return b
}

func (b *TokenBucket) ReduceTokensFromBucket(tokenCount int) (*TokenBucket, error) {
    if b.availableTokens-tokenCount < 0 {
        return nil, errors.New("not enough tokens")
    }

    b.availableTokens -= tokenCount
    return b, nil
}

func (b *TokenBucket) GetTokenBucketState() *TokenBucket {
    return b
}

func NewTokenBucket() *TokenBucket {
    return &TokenBucket{
        availableTokens: MAX_TOKEN_CAPACITY,
        lastUpdateTime:  currentUnixTime(),
    }
}

// helper functions
func min(a, b int) int {
    if a < b {
        return a
    }
    return b
}

func currentUnixTime() int {
    return int(time.Now().Unix())
}

func main() {
    tokenBucket := NewTokenBucket()
    fmt.Printf("bucket current state: %#v\n", tokenBucket.GetTokenBucketState())

    tokenBucket, err := tokenBucket.ReduceTokensFromBucket(7)
    if err != nil {
        fmt.Println(err.Error())
    }

    // adding sleep to fill bucket
    time.Sleep(2 * time.Second)
    fmt.Printf("bucket updated state: %#v\n", tokenBucket.RefillTokenBucket().GetTokenBucketState())

    _, err = tokenBucket.ReduceTokensFromBucket(8)
    if err != nil {
        fmt.Println(err.Error())
    }
}
ย