ratelimiter

package
v0.0.0-...-6067653 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 11, 2025 License: Apache-2.0 Imports: 9 Imported by: 0

README

ratelimiter

Token bucket rate limiter with in-memory storage, HTTP middleware, and configurable options.

Features

  • Token bucket algorithm with configurable capacity and refill rate
  • In-memory store with automatic cleanup of stale buckets
  • HTTP middleware with standard rate limit headers
  • Composite key functions for complex rate limiting scenarios
  • Thread-safe operations with proper error handling

Installation

go get github.com/dmitrymomot/saaskit/pkg/ratelimiter

Usage

package main

import (
    "context"
    "log"
    "net/http"
    "time"

    "github.com/dmitrymomot/saaskit/pkg/ratelimiter"
)

func main() {
    // Create rate limiter configuration
    config := ratelimiter.Config{
        Capacity:       100,           // 100 requests max burst
        RefillRate:     10,            // 10 tokens per interval
        RefillInterval: time.Second,   // Refill every second
    }

    // Create memory store with cleanup
    store := ratelimiter.NewMemoryStore()
    defer store.Close()

    // Create token bucket limiter
    limiter, err := ratelimiter.NewBucket(store, config)
    if err != nil {
        log.Fatal(err)
    }

    // Use with HTTP middleware
    keyFunc := func(r *http.Request) string {
        return r.RemoteAddr
    }

    middleware := ratelimiter.Middleware(limiter, keyFunc)

    handler := middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello, World!"))
    }))

    http.ListenAndServe(":8080", handler)
}

Common Operations

Direct Rate Limiting
// Check if request is allowed
result, err := limiter.Allow(ctx, "user:123")
if err != nil {
    // Handle error
}

if !result.Allowed() {
    // Rate limit exceeded
    retryAfter := result.RetryAfter()
}
Bulk Token Consumption
// Consume multiple tokens at once
result, err := limiter.AllowN(ctx, "user:123", 5)
if err != nil {
    // Handle error
}
Status Check Without Consumption
// Check current bucket state without consuming tokens
result, err := limiter.Status(ctx, "user:123")
if err != nil {
    // Handle error
}

remaining := result.Remaining
resetAt := result.ResetAt
Composite Key Functions
// Combine multiple key extractors
keyFunc := ratelimiter.Composite(
    func(r *http.Request) string { return r.Header.Get("X-API-Key") },
    func(r *http.Request) string { return r.RemoteAddr },
)

middleware := ratelimiter.Middleware(limiter, keyFunc)

Error Handling

import "errors"

result, err := limiter.Allow(ctx, key)
if err != nil {
    if errors.Is(err, ratelimiter.ErrInvalidTokenCount) {
        // Invalid token count
    } else if errors.Is(err, ratelimiter.ErrInvalidConfig) {
        // Invalid configuration
    } else if errors.Is(err, ratelimiter.ErrStoreUnavailable) {
        // Store backend unavailable
    }
}

Configuration

Memory Store Options
store := ratelimiter.NewMemoryStore(
    ratelimiter.WithCleanupInterval(10 * time.Minute), // Custom cleanup interval
)

// Disable cleanup
store := ratelimiter.NewMemoryStore(
    ratelimiter.WithCleanupInterval(0),
)
Custom Error Responder
errorResponder := func(w http.ResponseWriter, r *http.Request, result *ratelimiter.Result, err error) {
    if err != nil {
        http.Error(w, "Service unavailable", http.StatusServiceUnavailable)
        return
    }

    if result != nil && !result.Allowed() {
        w.Header().Set("Retry-After", strconv.Itoa(int(result.RetryAfter().Seconds())))
        http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
    }
}

middleware := ratelimiter.Middleware(limiter, keyFunc,
    ratelimiter.WithErrorResponder(errorResponder),
)

API Documentation

For detailed API documentation:

go doc -all ./pkg/ratelimiter

Or visit pkg.go.dev for online documentation.

Notes

  • Memory store automatically cleans up stale buckets (default: 5 minutes interval, 1 hour threshold)
  • Keys are automatically hashed when they exceed 64 characters to prevent unbounded storage growth
  • HTTP middleware adds standard rate limit headers: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset
  • Thread-safe for concurrent use across multiple goroutines

Documentation

Overview

Package ratelimiter provides token bucket rate limiting with memory storage and HTTP middleware.

The package implements a token bucket algorithm that allows burst traffic up to a configured capacity while maintaining a steady refill rate. It includes an in-memory storage backend with automatic cleanup and HTTP middleware for easy integration into web applications.

Basic Usage

Create a rate limiter with a memory store:

config := ratelimiter.Config{
	Capacity:       100,         // Maximum tokens (burst capacity)
	RefillRate:     10,          // Tokens added per interval
	RefillInterval: time.Second, // Refill frequency
}

store := ratelimiter.NewMemoryStore()
defer store.Close()

limiter, err := ratelimiter.NewBucket(store, config)
if err != nil {
	log.Fatal(err)
}

// Check if a request is allowed
result, err := limiter.Allow(ctx, "user:123")
if err != nil {
	// Handle error
	return
}

if !result.Allowed() {
	// Rate limit exceeded, retry after result.RetryAfter()
	return
}

HTTP Middleware

Use the provided middleware for HTTP rate limiting:

// Simple IP-based rate limiting
keyFunc := func(r *http.Request) string {
	return r.RemoteAddr
}

middleware := ratelimiter.Middleware(limiter, keyFunc)

handler := middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("Hello, World!"))
}))

http.ListenAndServe(":8080", handler)

The middleware automatically sets standard rate limit headers:

  • X-RateLimit-Limit: Maximum tokens
  • X-RateLimit-Remaining: Tokens remaining
  • X-RateLimit-Reset: Unix timestamp of next refill

Composite Key Functions

Combine multiple key extractors for complex rate limiting scenarios:

keyFunc := ratelimiter.Composite(
	func(r *http.Request) string { return r.Header.Get("X-API-Key") },
	func(r *http.Request) string { return r.RemoteAddr },
)

Keys longer than 64 characters are automatically hashed using FNV-1a to prevent unbounded storage growth.

Advanced Operations

Consume multiple tokens at once:

result, err := limiter.AllowN(ctx, "user:123", 5)
if err != nil {
	return err
}

Check bucket status without consuming tokens:

result, err := limiter.Status(ctx, "user:123")
if err != nil {
	return err
}

fmt.Printf("Remaining: %d, Reset at: %v\n",
	result.Remaining, result.ResetAt)

Reset a bucket (useful for administrative operations):

err := limiter.Reset(ctx, "user:123")
if err != nil {
	return err
}

Memory Management

The MemoryStore automatically cleans up stale buckets to prevent memory leaks:

store := ratelimiter.NewMemoryStore(
	ratelimiter.WithCleanupInterval(10 * time.Minute),
)

Buckets are considered stale if they haven't been accessed for 1 hour. Disable cleanup by setting the interval to 0.

Custom Error Handling

Customize error responses in the HTTP middleware:

errorResponder := func(w http.ResponseWriter, r *http.Request, result *ratelimiter.Result, err error) {
	if err != nil {
		http.Error(w, "Service temporarily unavailable", http.StatusServiceUnavailable)
		return
	}

	if result != nil && !result.Allowed() {
		retryAfter := int(result.RetryAfter().Seconds())
		w.Header().Set("Retry-After", strconv.Itoa(retryAfter))
		http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
	}
}

middleware := ratelimiter.Middleware(limiter, keyFunc,
	ratelimiter.WithErrorResponder(errorResponder),
)

Error Types

The package defines several error types for different failure scenarios:

if errors.Is(err, ratelimiter.ErrInvalidConfig) {
	// Configuration validation failed
}
if errors.Is(err, ratelimiter.ErrInvalidTokenCount) {
	// Token count must be positive
}
if errors.Is(err, ratelimiter.ErrStoreUnavailable) {
	// Storage backend is unavailable
}
if errors.Is(err, ratelimiter.ErrContextCancelled) {
	// Operation cancelled due to context
}

Thread Safety

All operations are thread-safe and can be used concurrently across multiple goroutines. The MemoryStore uses read-write mutexes for optimal performance with concurrent access.

Token Bucket Algorithm

The implementation uses the standard token bucket algorithm:

  1. Tokens are added to the bucket at the configured RefillRate and RefillInterval
  2. Each request consumes one or more tokens
  3. If insufficient tokens are available, the request is denied
  4. The bucket capacity limits the maximum burst size

This provides smooth rate limiting with burst tolerance, making it suitable for web APIs, user rate limiting, and resource protection scenarios.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrInvalidConfig indicates that the provided configuration is invalid.
	ErrInvalidConfig = errors.New("invalid configuration")

	// ErrInvalidTokenCount indicates that the requested token count is invalid.
	ErrInvalidTokenCount = errors.New("invalid token count")

	// ErrContextCancelled indicates that the operation was cancelled due to context.
	ErrContextCancelled = errors.New("context cancelled")

	// ErrStoreUnavailable indicates that the store backend is unavailable.
	ErrStoreUnavailable = errors.New("store unavailable")
)

Package-level error definitions for rate limiter operations.

Functions

func Middleware

func Middleware(limiter RateLimiter, keyFunc KeyFunc, opts ...MiddlewareOption) func(http.Handler) http.Handler

Middleware creates an HTTP middleware for rate limiting.

Types

type Bucket

type Bucket struct {
	// contains filtered or unexported fields
}

Bucket implements a token bucket rate limiter.

func NewBucket

func NewBucket(store Store, config Config) (*Bucket, error)

NewBucket creates a new token bucket rate limiter.

func (*Bucket) Allow

func (tb *Bucket) Allow(ctx context.Context, key string) (*Result, error)

func (*Bucket) AllowN

func (tb *Bucket) AllowN(ctx context.Context, key string, n int) (*Result, error)

func (*Bucket) Reset

func (tb *Bucket) Reset(ctx context.Context, key string) error

func (*Bucket) Status

func (tb *Bucket) Status(ctx context.Context, key string) (*Result, error)

Status returns the current state without consuming tokens.

type Config

type Config struct {
	Capacity       int           // Maximum tokens (burst capacity)
	RefillRate     int           // Tokens added per interval
	RefillInterval time.Duration // How frequently tokens are added
}

Config defines the token bucket rate limiting parameters.

type ErrorResponder

type ErrorResponder func(w http.ResponseWriter, r *http.Request, result *Result, err error)

ErrorResponder handles error responses for rate limiting. If err is not nil, it indicates an internal error. If err is nil and result.Allowed() is false, the rate limit was exceeded.

type KeyFunc

type KeyFunc func(r *http.Request) string

KeyFunc extracts a rate limit key from the request.

func Composite

func Composite(keyFuncs ...KeyFunc) KeyFunc

Composite combines multiple key functions into one rate limiting key. Uses FNV-1a hashing to keep keys under 64 characters for storage efficiency.

type MemoryStore

type MemoryStore struct {
	// contains filtered or unexported fields
}

MemoryStore implements Store interface using in-memory storage.

func NewMemoryStore

func NewMemoryStore(opts ...MemoryStoreOption) *MemoryStore

NewMemoryStore creates a new in-memory store with optional cleanup.

func (*MemoryStore) Close

func (ms *MemoryStore) Close()

Close stops the cleanup goroutine. Safe to call multiple times.

func (*MemoryStore) ConsumeTokens

func (ms *MemoryStore) ConsumeTokens(ctx context.Context, key string, tokens int, config Config) (remaining int, resetAt time.Time, err error)

ConsumeTokens attempts to consume tokens from the bucket.

func (*MemoryStore) Reset

func (ms *MemoryStore) Reset(ctx context.Context, key string) error

type MemoryStoreOption

type MemoryStoreOption func(*MemoryStore)

MemoryStoreOption configures a MemoryStore.

func WithCleanupInterval

func WithCleanupInterval(interval time.Duration) MemoryStoreOption

WithCleanupInterval sets the cleanup interval for removing stale buckets. Set to 0 to disable automatic cleanup.

type MiddlewareOption

type MiddlewareOption func(*middlewareConfig)

MiddlewareOption configures the rate limiting middleware.

func WithErrorResponder

func WithErrorResponder(responder ErrorResponder) MiddlewareOption

WithErrorResponder sets a custom error responder.

type RateLimiter

type RateLimiter interface {
	Allow(ctx context.Context, key string) (*Result, error)
	AllowN(ctx context.Context, key string, n int) (*Result, error)
}

RateLimiter defines the interface for rate limiting implementations.

type Result

type Result struct {
	Limit     int       // Maximum tokens (bucket capacity)
	Remaining int       // Tokens remaining (negative means denied)
	ResetAt   time.Time // When next token refill occurs
}

Result contains the result of a rate limit check.

func (*Result) Allowed

func (r *Result) Allowed() bool

func (*Result) RetryAfter

func (r *Result) RetryAfter() time.Duration

RetryAfter returns how long to wait before retry is likely to succeed. Returns 0 if the request was allowed.

type Store

type Store interface {
	// ConsumeTokens attempts to consume tokens and returns the state after consumption.
	// If tokens is 0, updates bucket state without consuming (used for status checks).
	// A negative remaining count indicates the request should be denied.
	ConsumeTokens(ctx context.Context, key string, tokens int, config Config) (remaining int, resetAt time.Time, err error)

	Reset(ctx context.Context, key string) error
}

Store defines the interface for rate limit storage backends.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL