Skip to main content

Overview

The ratelimit middleware implements token bucket rate limiting to control request rates. It protects your application from abuse, ensures fair usage, and prevents resource exhaustion. Use it when you need:
  • API rate limiting
  • Protection against brute force attacks
  • Fair usage enforcement
  • Resource protection

Installation

import "github.com/go-mizu/mizu/middlewares/ratelimit"

Quick Start

app := mizu.New()

// 100 requests per minute
app.Use(ratelimit.PerMinute(100))

Configuration

Options

OptionTypeDefaultDescription
Rateint100Requests allowed per interval
Intervaltime.Duration1mTime window
BurstintSame as RateMaximum burst capacity
KeyFuncfunc(*mizu.Ctx) stringClient IPRate limit key extractor
HeadersbooltrueInclude rate limit headers
ErrorHandlerfunc(*mizu.Ctx) error-Custom error handler
Skipfunc(*mizu.Ctx) bool-Skip rate limiting

Examples

Basic Rate Limiting

// 100 requests per minute per IP
app.Use(ratelimit.PerMinute(100))

// 10 requests per second
app.Use(ratelimit.PerSecond(10))

// 1000 requests per hour
app.Use(ratelimit.PerHour(1000))

Custom Rate and Interval

// 50 requests per 30 seconds
app.Use(ratelimit.New(50, 30*time.Second))

With Burst

// Allow burst of 200, refill at 100/minute
app.Use(ratelimit.WithOptions(ratelimit.Options{
    Rate:     100,
    Interval: time.Minute,
    Burst:    200,
}))

Rate Limit by API Key

app.Use(ratelimit.WithOptions(ratelimit.Options{
    Rate:     1000,
    Interval: time.Hour,
    KeyFunc: func(c *mizu.Ctx) string {
        // Use API key instead of IP
        return c.Request().Header.Get("X-API-Key")
    },
}))

Rate Limit by User

app.Use(ratelimit.WithOptions(ratelimit.Options{
    Rate:     100,
    Interval: time.Minute,
    KeyFunc: func(c *mizu.Ctx) string {
        if user := GetUser(c); user != nil {
            return user.ID
        }
        return c.ClientIP() // Fallback to IP
    },
}))

Skip Certain Requests

app.Use(ratelimit.WithOptions(ratelimit.Options{
    Rate:     100,
    Interval: time.Minute,
    Skip: func(c *mizu.Ctx) bool {
        // Skip health checks
        if c.Request().URL.Path == "/health" {
            return true
        }
        // Skip authenticated admins
        if user := GetUser(c); user != nil && user.IsAdmin {
            return true
        }
        return false
    },
}))

Custom Error Handler

app.Use(ratelimit.WithOptions(ratelimit.Options{
    Rate:     100,
    Interval: time.Minute,
    ErrorHandler: func(c *mizu.Ctx) error {
        return c.JSON(429, map[string]any{
            "error":       "Rate limit exceeded",
            "retry_after": c.Header().Get("Retry-After"),
        })
    },
}))

Disable Headers

app.Use(ratelimit.WithOptions(ratelimit.Options{
    Rate:     100,
    Interval: time.Minute,
    Headers:  false, // Don't include rate limit headers
}))

Different Limits for Different Routes

// Global: 100 requests/minute
app.Use(ratelimit.PerMinute(100))

// API: More restrictive
api := app.Group("/api")
api.Use(ratelimit.PerMinute(60))

// Expensive operations: Very restrictive
app.Post("/api/export", exportHandler, ratelimit.PerMinute(5))

Tiered Rate Limits

func tierRateLimit() mizu.Middleware {
    free := ratelimit.WithOptions(ratelimit.Options{
        Rate: 100, Interval: time.Hour,
        KeyFunc: func(c *mizu.Ctx) string { return "free:" + c.ClientIP() },
    })

    pro := ratelimit.WithOptions(ratelimit.Options{
        Rate: 10000, Interval: time.Hour,
        KeyFunc: func(c *mizu.Ctx) string { return "pro:" + GetUser(c).ID },
    })

    return func(next mizu.Handler) mizu.Handler {
        return func(c *mizu.Ctx) error {
            user := GetUser(c)
            if user != nil && user.Plan == "pro" {
                return pro(next)(c)
            }
            return free(next)(c)
        }
    }
}

Custom Store

// Implement Store interface for Redis, etc.
type redisStore struct {
    client *redis.Client
}

func (s *redisStore) Allow(key string, rate int, interval time.Duration, burst int) (bool, ratelimit.RateLimitInfo) {
    // Redis implementation
}

app.Use(ratelimit.WithStore(&redisStore{client}, ratelimit.Options{
    Rate:     100,
    Interval: time.Minute,
}))

Response Headers

When Headers is enabled (default), these headers are included:
HeaderDescription
X-RateLimit-LimitMaximum requests allowed
X-RateLimit-RemainingRemaining requests in window
X-RateLimit-ResetUnix timestamp when limit resets
Retry-AfterSeconds until retry (when limited)

API Reference

Functions

// Convenience constructors
func PerSecond(n int) mizu.Middleware
func PerMinute(n int) mizu.Middleware
func PerHour(n int) mizu.Middleware

// Custom rate and interval
func New(rate int, interval time.Duration) mizu.Middleware

// Full configuration
func WithOptions(opts Options) mizu.Middleware

// Custom store
func WithStore(store Store, opts Options) mizu.Middleware

Store Interface

type Store interface {
    Allow(key string, rate int, interval time.Duration, burst int) (bool, RateLimitInfo)
}

RateLimitInfo

type RateLimitInfo struct {
    Limit     int
    Remaining int
    Reset     time.Time
}

Token Bucket Algorithm

The middleware uses the token bucket algorithm:
  1. Bucket starts full with Burst tokens
  2. Requests consume one token
  3. Tokens refill at Rate per Interval
  4. Burst allows temporary spikes
Bucket: [β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ] 100 tokens
Request β†’ [β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ ] 99 tokens
Request β†’ [β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ  ] 98 tokens
...
Refill  β†’ [β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ] 100 tokens (after interval)

Technical Details

The rate limit middleware implements the token bucket algorithm, a sophisticated approach for managing request rates with support for burst traffic. The implementation details are as follows:

Token Bucket Implementation

The algorithm maintains a bucket of tokens for each rate limit key:
  1. Initialization: Each bucket starts with Burst tokens (default equals Rate)
  2. Token Consumption: Each request consumes exactly one token from the bucket
  3. Token Refill: Tokens are added continuously based on elapsed time:
    • Refill rate: Rate / Interval tokens per second
    • Formula: tokensToAdd = (Rate * elapsedSeconds) / intervalSeconds
  4. Capacity Limit: Bucket capacity is capped at Burst tokens to prevent unlimited accumulation
  5. Request Decision: Request is allowed if bucket has at least 1 token available

In-Memory Store

The default MemoryStore provides:
  • Thread Safety: Uses sync.Mutex for concurrent access protection
  • Per-Key Tracking: Maintains separate buckets for each key (IP, user ID, API key, etc.)
  • Automatic Cleanup: Background goroutine removes stale buckets after 10 minutes of inactivity
  • Fractional Tokens: Uses float64 for precise token calculations during refills

Rate Limit Information

For each request, the middleware calculates and returns:
  • Limit: Maximum requests allowed per interval (from Rate option)
  • Remaining: Current token count (floored to nearest integer)
  • Reset: Timestamp when bucket will be full again (current time + Interval)

Best Practices

  • Set reasonable limits based on expected usage
  • Use different limits for different endpoints
  • Include rate limit headers for client awareness
  • Monitor rate limit hits for tuning
  • Consider user tiers for fair access

Testing

The rate limit middleware includes comprehensive test coverage for all functionality:
Test CaseDescriptionExpected Behavior
TestNewBasic rate limiting with IP-based keysFirst N requests succeed, subsequent requests return 429; different IPs tracked separately
TestWithOptions_HeadersRate limit headers in responseHeaders include X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset
TestWithOptions_ErrorHandlerCustom error handler for rate limit exceededCustom error handler called instead of default when limit exceeded
TestWithOptions_KeyFuncCustom key extraction functionRate limits applied per custom key (e.g., API key) instead of IP
TestWithOptions_SkipSkip rate limiting for certain requestsRequests matching skip condition bypass rate limiting
TestWithOptions_RetryAfterRetry-After header on rate limitRetry-After header included when request is rate limited
TestPerSecondPerSecond convenience functionMiddleware created with per-second interval
TestPerMinutePerMinute convenience functionMiddleware created with per-minute interval
TestPerHourPerHour convenience functionMiddleware created with per-hour interval
TestMemoryStore_AllowToken bucket allows/denies requestsAllows up to limit, denies when bucket empty
TestMemoryStore_TokenRefillToken bucket refills over timeTokens refill after interval allowing new requests
TestMemoryStore_DifferentKeysDifferent keys have independent bucketsExhausting one key doesn’t affect other keys