limited

package module
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Jun 12, 2025 License: MIT Imports: 7 Imported by: 1

README

Limited - DynamoDB-based Rate Limiting for Go

Limited is a flexible, high-performance rate limiting library for Go that uses AWS DynamoDB as its backend storage. It supports multiple rate limiting strategies and provides atomic operations for distributed systems.

Features

  • 🚀 High Performance: Atomic check-and-increment operations using DynamoDB conditional updates
  • 🌍 Distributed: Works seamlessly across multiple servers/instances
  • 📊 Multiple Strategies: Fixed window, sliding window, and multi-window rate limiting
  • DynamORM Integration: Built on top of Pay Theory's DynamORM for efficient DynamoDB operations
  • 🔧 Flexible Configuration: Per-identifier and per-resource limits
  • 🛡️ Fail-Open Option: Continue serving requests even if rate limiter fails
  • 📈 Usage Statistics: Track and query usage patterns
  • 🔌 HTTP Middleware: Ready-to-use HTTP middleware with customizable options

Installation

go get github.com/pay-theory/limited

Quick Start

package main

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

    "github.com/pay-theory/dynamorm/pkg/core"
    "github.com/pay-theory/limited"
    "github.com/pay-theory/limited/middleware"
    "go.uber.org/zap"
)

func main() {
    // Initialize DynamoDB connection
    db, err := core.NewDB(core.Config{
        Region:    "us-east-1",
        TableName: "rate-limits",
    })
    if err != nil {
        log.Fatal(err)
    }

    // Create a rate limiter with fixed window strategy
    strategy := limited.NewFixedWindowStrategy(time.Minute, 100) // 100 requests per minute
    
    limiter := limited.NewDynamoRateLimiter(
        db,
        nil,     // Use default config
        strategy,
        zap.NewExample(),
    )

    // Create HTTP middleware
    rateLimitMiddleware := middleware.Middleware(middleware.Options{
        Limiter: limiter,
        ExtractIdentifier: func(r *http.Request) string {
            // Use API key from header, fallback to IP
            if apiKey := r.Header.Get("X-API-Key"); apiKey != "" {
                return apiKey
            }
            return r.Header.Get("X-Real-IP")
        },
    })

    // Apply middleware to your handler
    http.HandleFunc("/api/endpoint", rateLimitMiddleware(handleRequest))
    
    log.Fatal(http.ListenAndServe(":8080", nil))
}

func handleRequest(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Hello, World!"))
}

Strategies

Fixed Window

Limits requests within fixed time windows (e.g., 100 requests per minute starting at :00 seconds).

strategy := limited.NewFixedWindowStrategy(time.Minute, 100)
Sliding Window

Provides more accurate rate limiting by checking multiple sub-windows.

strategy := limited.NewSlidingWindowStrategy(
    time.Hour,        // Window size
    1000,            // Max requests per hour
    time.Minute,     // Granularity (check every minute)
)
Multi-Window

Enforces multiple limits simultaneously (e.g., 100/minute AND 1000/hour).

strategy := limited.NewMultiWindowStrategy([]limited.WindowConfig{
    {Duration: time.Minute, MaxRequests: 100},
    {Duration: time.Hour, MaxRequests: 1000},
})

Configuration

config := &limited.Config{
    DefaultRequestsPerHour:   1000,
    DefaultRequestsPerMinute: 100,
    EnableBurstCapacity:      true,
    FailOpen:                 true,  // Allow requests if rate limiter fails
    TableName:                "my-rate-limits",
    TTLHours:                 24,    // Auto-cleanup old entries
    
    // Per-identifier limits
    IdentifierLimits: map[string]limited.Limit{
        "premium-user": {
            RequestsPerHour:   10000,
            RequestsPerMinute: 1000,
        },
    },
    
    // Per-resource limits
    ResourceLimits: map[string]limited.Limit{
        "/api/expensive-operation": {
            RequestsPerHour:   100,
            RequestsPerMinute: 10,
        },
    },
}

DynamoDB Table Setup

Create a DynamoDB table with the following configuration:

TableName: rate-limits
PartitionKey: PK (String)
SortKey: SK (String)
TTL: TTL (Number) - Enable TTL on this attribute
AWS CLI Command
aws dynamodb create-table \
    --table-name rate-limits \
    --attribute-definitions \
        AttributeName=PK,AttributeType=S \
        AttributeName=SK,AttributeType=S \
    --key-schema \
        AttributeName=PK,KeyType=HASH \
        AttributeName=SK,KeyType=RANGE \
    --billing-mode PAY_PER_REQUEST

# Enable TTL
aws dynamodb update-time-to-live \
    --table-name rate-limits \
    --time-to-live-specification \
        Enabled=true,AttributeName=TTL

Advanced Usage

Custom Middleware Options
middleware.Middleware(middleware.Options{
    Limiter: limiter,
    
    // Custom identifier extraction
    ExtractIdentifier: func(r *http.Request) string {
        return r.Header.Get("X-User-ID")
    },
    
    // Custom resource extraction
    ExtractResource: func(r *http.Request) string {
        // Group endpoints by prefix
        if strings.HasPrefix(r.URL.Path, "/api/v1/") {
            return "api-v1"
        }
        return r.URL.Path
    },
    
    // Skip health checks
    SkipRequest: func(r *http.Request) bool {
        return r.URL.Path == "/health"
    },
    
    // Custom error response
    ErrorHandler: func(w http.ResponseWriter, r *http.Request, decision *limited.LimitDecision) {
        w.Header().Set("Content-Type", "application/json")
        w.WriteHeader(http.StatusTooManyRequests)
        json.NewEncoder(w).Encode(map[string]interface{}{
            "error": "rate_limit_exceeded",
            "retry_after": decision.RetryAfter.Seconds(),
        })
    },
    
    // Metrics collection
    OnRateLimit: func(r *http.Request, decision *limited.LimitDecision) {
        metrics.IncrCounter("rate_limit_exceeded", map[string]string{
            "resource": decision.Resource,
        })
    },
})
Direct Usage (without middleware)
// Check and increment atomically
decision, err := limiter.CheckAndIncrement(ctx, limited.RateLimitKey{
    Identifier: "user-123",
    Resource:   "api/users",
    Operation:  "GET",
})

if err != nil {
    // Handle error
}

if !decision.Allowed {
    // Request is rate limited
    fmt.Printf("Rate limited. Retry after %v\n", decision.RetryAfter)
    return
}

// Process request...
Query Usage Statistics
stats, err := limiter.GetUsage(ctx, limited.RateLimitKey{
    Identifier: "user-123",
    Resource:   "api/users",
    Operation:  "GET",
})

if err != nil {
    // Handle error
}

fmt.Printf("Current minute: %d/%d requests\n", 
    stats.CurrentMinute.Count, 
    stats.CurrentMinute.Limit)
fmt.Printf("Current hour: %d/%d requests\n", 
    stats.CurrentHour.Count, 
    stats.CurrentHour.Limit)

Testing

For testing, you can use a mock clock:

type MockClock struct {
    CurrentTime time.Time
}

func (m *MockClock) Now() time.Time {
    return m.CurrentTime
}

// In your test
mockClock := &MockClock{CurrentTime: time.Now()}
limiter.SetClock(mockClock)

// Advance time
mockClock.CurrentTime = mockClock.CurrentTime.Add(time.Minute)

Performance Considerations

  1. Atomic Operations: The CheckAndIncrement method provides the best performance for high-throughput scenarios
  2. Batch Operations: For bulk operations, consider implementing batch checks
  3. TTL: Set appropriate TTL values to prevent table growth
  4. Consistent Reads: Disable for better performance if eventual consistency is acceptable

License

MIT License - see LICENSE file for details

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

Support

For issues, questions, or contributions, please visit github.com/pay-theory/limited

Documentation

Overview

Package limited provides DynamoDB-based rate limiting

Package limited provides DynamoDB-based rate limiting

Package limited provides DynamoDB-based rate limiting

Package limited provides DynamoDB-based rate limiting

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type AtomicRateLimiter

type AtomicRateLimiter interface {
	RateLimiter
	// CheckAndIncrement performs an atomic check and increment operation
	// Returns the new count and whether the request is allowed
	CheckAndIncrement(ctx context.Context, key RateLimitKey) (*LimitDecision, error)
}

AtomicRateLimiter extends RateLimiter with atomic check-and-increment support

type Clock

type Clock interface {
	Now() time.Time
}

Clock interface for time operations (allows mocking in tests)

type Config

type Config struct {
	// Default limits
	DefaultRequestsPerHour   int
	DefaultRequestsPerMinute int
	DefaultBurstCapacity     int

	// Feature flags
	EnableBurstCapacity bool
	EnableSoftLimits    bool // Warn but don't block
	FailOpen            bool // Allow requests if rate limiter fails

	// Storage settings
	TableName      string
	ConsistentRead bool
	TTLHours       int

	// Custom limits by identifier
	IdentifierLimits map[string]Limit

	// Custom limits by resource
	ResourceLimits map[string]Limit
}

Config contains configuration for the rate limiter

func DefaultConfig

func DefaultConfig() *Config

DefaultConfig returns a default configuration

type DynamoRateLimiter

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

DynamoRateLimiter implements RateLimiter using DynamoDB via DynamORM

func NewDynamoRateLimiter

func NewDynamoRateLimiter(db core.DB, config *Config, strategy RateLimitStrategy, logger *zap.Logger) *DynamoRateLimiter

NewDynamoRateLimiter creates a new DynamoDB-based rate limiter

func (*DynamoRateLimiter) CheckAndIncrement

func (r *DynamoRateLimiter) CheckAndIncrement(ctx context.Context, key RateLimitKey) (*LimitDecision, error)

CheckAndIncrement performs an atomic check and increment operation

func (*DynamoRateLimiter) CheckLimit

func (r *DynamoRateLimiter) CheckLimit(ctx context.Context, key RateLimitKey) (*LimitDecision, error)

CheckLimit checks if a request is allowed based on rate limits

func (*DynamoRateLimiter) GetUsage

func (r *DynamoRateLimiter) GetUsage(ctx context.Context, key RateLimitKey) (*UsageStats, error)

GetUsage returns current usage statistics for a rate limit key

func (*DynamoRateLimiter) RecordRequest

func (r *DynamoRateLimiter) RecordRequest(ctx context.Context, key RateLimitKey) error

RecordRequest records that a request was made using atomic increment

func (*DynamoRateLimiter) SetClock

func (r *DynamoRateLimiter) SetClock(clock Clock)

SetClock sets the clock for testing

type Error

type Error struct {
	Type    ErrorType
	Message string
	Cause   error
}

Error represents a rate limiter error

func NewError

func NewError(errorType ErrorType, message string) *Error

NewError creates a new rate limiter error

func WrapError

func WrapError(cause error, errorType ErrorType, message string) *Error

WrapError wraps an error with additional context

func (*Error) Error

func (e *Error) Error() string

Error implements the error interface

func (*Error) Unwrap

func (e *Error) Unwrap() error

Unwrap returns the underlying error

type ErrorType

type ErrorType string

Error types for rate limiting

const (
	ErrorTypeInternal     ErrorType = "internal_error"
	ErrorTypeRateLimit    ErrorType = "rate_limit_exceeded"
	ErrorTypeInvalidInput ErrorType = "invalid_input"
)

type FixedWindowStrategy

type FixedWindowStrategy struct {
	WindowSize       time.Duration
	MaxRequests      int
	IdentifierLimits map[string]int // Identifier-specific overrides
	ResourceLimits   map[string]int // Resource-specific overrides
}

FixedWindowStrategy implements fixed window rate limiting

func NewFixedWindowStrategy

func NewFixedWindowStrategy(windowSize time.Duration, maxRequests int) *FixedWindowStrategy

NewFixedWindowStrategy creates a new fixed window strategy

func (*FixedWindowStrategy) CalculateWindows

func (s *FixedWindowStrategy) CalculateWindows(now time.Time) []TimeWindow

CalculateWindows returns the time windows to check for the current time

func (*FixedWindowStrategy) GetLimit

func (s *FixedWindowStrategy) GetLimit(key RateLimitKey) int

GetLimit returns the limit for a given key

func (*FixedWindowStrategy) SetIdentifierLimit

func (s *FixedWindowStrategy) SetIdentifierLimit(identifier string, limit int)

SetIdentifierLimit sets a limit override for a specific identifier

func (*FixedWindowStrategy) SetResourceLimit

func (s *FixedWindowStrategy) SetResourceLimit(resource string, limit int)

SetResourceLimit sets a limit override for a specific resource

func (*FixedWindowStrategy) ShouldAllow

func (s *FixedWindowStrategy) ShouldAllow(counts map[string]int, limit int) bool

ShouldAllow determines if a request should be allowed given current counts

type Limit

type Limit struct {
	RequestsPerHour   int
	RequestsPerMinute int
	BurstCapacity     int
	CustomWindows     map[string]WindowLimit // e.g., "5m": {Requests: 100}
}

Limit defines rate limits for a specific entity

type LimitDecision

type LimitDecision struct {
	// Allowed indicates whether the request is allowed
	Allowed bool
	// CurrentCount is the current number of requests in the window
	CurrentCount int
	// Limit is the maximum allowed requests
	Limit int
	// ResetsAt is when the current window resets
	ResetsAt time.Time
	// RetryAfter is set if request is not allowed, indicating when to retry
	RetryAfter *time.Duration
}

LimitDecision represents the result of a rate limit check

type MultiWindowStrategy

type MultiWindowStrategy struct {
	Windows          []WindowConfig
	IdentifierLimits map[string][]WindowConfig // Identifier-specific overrides
	ResourceLimits   map[string][]WindowConfig // Resource-specific overrides
}

MultiWindowStrategy implements multiple window rate limiting (e.g., 100 requests per minute AND 1000 requests per hour)

func NewMultiWindowStrategy

func NewMultiWindowStrategy(windows []WindowConfig) *MultiWindowStrategy

NewMultiWindowStrategy creates a new multi-window strategy

func (*MultiWindowStrategy) CalculateWindows

func (s *MultiWindowStrategy) CalculateWindows(now time.Time) []TimeWindow

CalculateWindows returns all windows to check

func (*MultiWindowStrategy) GetLimit

func (s *MultiWindowStrategy) GetLimit(key RateLimitKey) int

GetLimit returns the limit for the first window (primary limit)

func (*MultiWindowStrategy) ShouldAllow

func (s *MultiWindowStrategy) ShouldAllow(counts map[string]int, limit int) bool

ShouldAllow checks all windows

type RateLimitEntry

type RateLimitEntry struct {
	// PK/SK pattern for composite keys
	PK         string `dynamorm:"pk" json:"pk"` // {identifier}#{window_start}
	SK         string `dynamorm:"sk" json:"sk"` // {resource}#{operation}
	Identifier string `json:"identifier"`       // e.g., partner_id, user_id
	Resource   string `json:"resource"`         // e.g., "api/v1/users"
	Operation  string `json:"operation"`        // e.g., "GET", "POST"

	// Window information
	WindowStart int64  `json:"window_start"` // Unix timestamp
	WindowType  string `json:"window_type"`  // "MINUTE", "HOUR", "CUSTOM"
	WindowID    string `json:"window_id"`    // For query purposes

	// Rate limit tracking
	Count int64 `json:"count"` // Counter (atomic via UpdateBuilder)

	// TTL for automatic cleanup
	TTL int64 `dynamorm:"ttl" json:"ttl"` // Unix timestamp for expiration

	// Metadata
	CreatedAt time.Time         `dynamorm:"created_at" json:"created_at"`
	UpdatedAt time.Time         `dynamorm:"updated_at" json:"updated_at"`
	Metadata  map[string]string `json:"metadata,omitempty"` // Additional context
}

RateLimitEntry tracks rate limit usage in DynamoDB

func (*RateLimitEntry) GetCompositeID

func (r *RateLimitEntry) GetCompositeID() string

GetCompositeID returns a unique identifier for the rate limit entry

func (*RateLimitEntry) SetKeys

func (r *RateLimitEntry) SetKeys()

SetKeys sets the PK and SK based on identifier, window_start, resource and operation

func (*RateLimitEntry) SetTTL

func (r *RateLimitEntry) SetTTL(windowDuration time.Duration, bufferDuration time.Duration)

SetTTL sets the TTL based on window duration and buffer time

func (RateLimitEntry) TableName

func (RateLimitEntry) TableName() string

TableName returns the table name for RateLimitEntry Can be overridden by setting LIMITED_TABLE_NAME environment variable

type RateLimitKey

type RateLimitKey struct {
	// Identifier is the primary identifier for the rate limit (e.g., partner_id, user_id, api_key)
	Identifier string
	// Resource identifies what resource is being accessed (e.g., "api/v1/users", "oauth/token")
	Resource string
	// Operation identifies the operation being performed (e.g., "GET", "POST", "READ", "WRITE")
	Operation string
	// Metadata provides additional context for the rate limit check
	Metadata map[string]string
}

RateLimitKey identifies a unique rate limit bucket

type RateLimitStrategy

type RateLimitStrategy interface {
	// CalculateWindows returns the time windows to check for the current time
	CalculateWindows(now time.Time) []TimeWindow

	// GetLimit returns the limit for a given key
	GetLimit(key RateLimitKey) int

	// ShouldAllow determines if a request should be allowed given current counts
	ShouldAllow(counts map[string]int, limit int) bool
}

RateLimitStrategy defines how rate limits are calculated

type RateLimitWindow

type RateLimitWindow struct {
	WindowType string // "MINUTE", "HOUR", "CUSTOM_<duration>"
	Start      time.Time
	End        time.Time
}

RateLimitWindow represents a time window for rate limiting

func GetDayWindow

func GetDayWindow(now time.Time) RateLimitWindow

GetDayWindow returns the current day window

func GetFixedWindow

func GetFixedWindow(now time.Time, duration time.Duration) RateLimitWindow

GetFixedWindow returns a fixed time window

func GetHourWindow

func GetHourWindow(now time.Time) RateLimitWindow

GetHourWindow returns the current hour window

func GetMinuteWindow

func GetMinuteWindow(now time.Time) RateLimitWindow

GetMinuteWindow returns the current minute window

type RateLimiter

type RateLimiter interface {
	// CheckLimit checks if a request is allowed based on rate limits
	CheckLimit(ctx context.Context, key RateLimitKey) (*LimitDecision, error)

	// RecordRequest records that a request was made (should be called after CheckLimit)
	RecordRequest(ctx context.Context, key RateLimitKey) error

	// GetUsage returns current usage statistics for a rate limit key
	GetUsage(ctx context.Context, key RateLimitKey) (*UsageStats, error)
}

RateLimiter defines the interface for rate limiting implementations

type RealClock

type RealClock struct{}

RealClock implements Clock using actual time

func (RealClock) Now

func (RealClock) Now() time.Time

Now returns the current time

type SlidingWindowStrategy

type SlidingWindowStrategy struct {
	WindowSize       time.Duration
	MaxRequests      int
	Granularity      time.Duration  // How fine-grained the sliding window is (e.g., 1 minute)
	IdentifierLimits map[string]int // Identifier-specific overrides
	ResourceLimits   map[string]int // Resource-specific overrides
}

SlidingWindowStrategy implements sliding window rate limiting

func NewSlidingWindowStrategy

func NewSlidingWindowStrategy(windowSize time.Duration, maxRequests int, granularity time.Duration) *SlidingWindowStrategy

NewSlidingWindowStrategy creates a new sliding window strategy

func (*SlidingWindowStrategy) CalculateWindows

func (s *SlidingWindowStrategy) CalculateWindows(now time.Time) []TimeWindow

CalculateWindows returns the time windows to check for sliding window

func (*SlidingWindowStrategy) GetLimit

func (s *SlidingWindowStrategy) GetLimit(key RateLimitKey) int

GetLimit returns the limit for a given key

func (*SlidingWindowStrategy) SetIdentifierLimit

func (s *SlidingWindowStrategy) SetIdentifierLimit(identifier string, limit int)

SetIdentifierLimit sets a limit override for a specific identifier

func (*SlidingWindowStrategy) SetResourceLimit

func (s *SlidingWindowStrategy) SetResourceLimit(resource string, limit int)

SetResourceLimit sets a limit override for a specific resource

func (*SlidingWindowStrategy) ShouldAllow

func (s *SlidingWindowStrategy) ShouldAllow(counts map[string]int, limit int) bool

ShouldAllow determines if a request should be allowed given current counts

type TimeWindow

type TimeWindow struct {
	Start time.Time
	End   time.Time
	Key   string // Used for storage key generation
}

TimeWindow represents a time period for rate limiting

type UsageStats

type UsageStats struct {
	Identifier    string
	Resource      string
	CurrentHour   UsageWindow
	CurrentMinute UsageWindow
	DailyTotal    int
	CustomWindows map[string]UsageWindow // For custom window sizes
}

UsageStats provides detailed usage information

type UsageWindow

type UsageWindow struct {
	Count       int
	Limit       int
	WindowStart time.Time
	WindowEnd   time.Time
}

UsageWindow represents usage within a time window

type WindowConfig

type WindowConfig struct {
	Duration    time.Duration
	MaxRequests int
}

WindowConfig defines a single window configuration

type WindowLimit

type WindowLimit struct {
	Duration time.Duration
	Requests int
}

WindowLimit defines limits for a custom time window

Directories

Path Synopsis
examples
basic command
Package middleware provides HTTP middleware for the limited rate limiter
Package middleware provides HTTP middleware for the limited rate limiter

Jump to

Keyboard shortcuts

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