ratelimit

package module
v1.1.0 Latest Latest
Warning

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

Go to latest
Published: Feb 19, 2026 License: MIT Imports: 5 Imported by: 0

README

Distributed Rate Limiter for Go

Go Reference Go Report Card

A high-performance, distributed rate-limiting library for Go, powered by Redis and Lua scripting. This library implements the Sliding Window Counter algorithm to ensure precision and atomicity across multiple service instances.

🚀 Features

  • Distributed Architecture: Synchronize rate limits across multiple nodes using Redis.
  • Sliding Window Algorithm: Prevents traffic bursts at window boundaries, offering better precision than Fixed Window.
  • Atomic Operations: Uses Redis Lua scripting to guarantee thread-safe operations without race conditions.
  • Weighted Requests: AllowN lets you consume multiple units in a single atomic call.
  • Read-only Peek: Status lets you inspect remaining quota without consuming it.
  • Framework Agnostic: Core logic is decoupled from web frameworks.
  • Production Ready: Built-in support for context.Context for timeout and cancellation handling.

🛠 Installation

go get github.com/vnmchuo/ratelimiter@v1.1.0

💡 Quick Start

Allow — single unit per call
package main

import (
    "context"
    "fmt"
    "time"

    "github.com/redis/go-redis/v9"
    ratelimiter "github.com/vnmchuo/ratelimiter"
)

func main() {
    rdb := redis.NewClient(&redis.Options{
        Addr: "localhost:6379",
    })

    // 100 requests per minute
    limiter := ratelimiter.NewRedisStore(
        rdb,
        ratelimiter.WithLimit(100),
        ratelimiter.WithWindow(time.Minute),
    )

    res, err := limiter.Allow(context.Background(), "user-123")
    if err != nil {
        panic(err)
    }

    if res.Allowed {
        fmt.Printf("OK — %d remaining\n", res.Remaining)
    } else {
        fmt.Println("Rate limit exceeded (HTTP 429)")
    }
}
AllowN — consume N units atomically

Use AllowN for weighted requests such as bulk API calls, large file uploads, or multi-item operations where a single request represents more than one logical unit of work.

// Consume 10 units at once (e.g., a batch request that fetches 10 records)
res, err := limiter.AllowN(context.Background(), "user-123", 10)
if err != nil {
    panic(err)
}

if res.Allowed {
    fmt.Printf("Batch accepted — %d units remaining\n", res.Remaining)
} else {
    fmt.Printf("Not enough quota (only %d units left)\n", res.Remaining)
}
Status — peek without consuming

Use Status to inspect remaining quota before committing to an expensive operation.

s, err := limiter.Status(context.Background(), "user-123")
if err != nil {
    panic(err)
}

fmt.Printf("Current usage: %d/%d — %d remaining\n",
    int64(s.Limit)-s.Remaining, int64(s.Limit), s.Remaining)

if s.Remaining >= 50 {
    // Safe to proceed with a large batch
}

Note: Status removes expired entries from the window for accuracy but never adds new ones. It is safe to call frequently with no side effects on quota.

🔌 Framework Integration

Gin Middleware
import (
    ginmw "github.com/vnmchuo/ratelimiter/middleware/gin"
)

r := gin.Default()
r.Use(ginmw.RateLimiter(limiter, func(c *gin.Context) string {
    return c.ClientIP() // key per IP
}))

The middleware automatically sets X-RateLimit-Limit and X-RateLimit-Remaining response headers and returns HTTP 429 when the limit is exceeded.

📊 Benchmarks

Benchmarks were run on an Intel Core i5-1145G7 @ 2.60GHz (Windows, amd64) using an in-process miniredis instance to eliminate network overhead:

goos: windows
goarch: amd64
cpu: 11th Gen Intel(R) Core(TM) i5-1145G7 @ 2.60GHz
Benchmark Iterations ns/op B/op allocs/op
BenchmarkRedisStore_Allow 10,000 973,820 ~34 KB 793
BenchmarkRedisStore_AllowN (n=5) 10,000 ~1,100,000 ~55 KB 971
BenchmarkRedisStore_Status 30,987 188,717 ~20 KB

Note: Production numbers with a networked Redis instance will be higher, dominated by round-trip latency (~1–5 ms). Status is faster than Allow/AllowN because it never writes to Redis.

To run benchmarks on your own hardware:

go test -bench=BenchmarkRedisStore -benchmem -benchtime=5s .

🧠 Algorithm Comparison

This library uses the Sliding Window Counter algorithm. Here's how it compares to common alternatives:

Property Fixed Window Token Bucket Sliding Window Counter
Burst at boundary ❌ Yes — 2× burst possible ✅ Controlled ✅ None — truly smooth
Memory per key ✅ O(1) ✅ O(1) ⚠️ O(limit) sorted set
Precision ❌ Low ✅ High ✅ High
Distributed safe ✅ With atomic INCR ⚠️ Complex to distribute ✅ Lua script atomicity
Weighted requests ⚠️ Possible ✅ Native ✅ Native (AllowN)
Implementation Simple Moderate Moderate

Why Sliding Window?

  • Fixed Window has a well-known double-burst vulnerability: a client can send limit requests just before a window resets and limit requests immediately after, effectively sending 2×limit in a short span.
  • Token Bucket handles bursts well but is harder to implement correctly in a distributed setting — you need per-node state or complex synchronization.
  • Sliding Window via a Redis sorted set gives exact per-key tracking over a rolling time range, with atomicity guaranteed by Lua scripting. The O(limit) memory cost per key is acceptable for typical API rate limits (e.g., ≤10,000 req/min/user).

⚠️ Known Limitations

Limitation Details
Redis dependency The library requires a running Redis instance. There is no in-process or local fallback. If Redis is unavailable, all Allow/AllowN/Status calls return an error — design your service to fail open or closed accordingly.
No local fallback There is no in-memory fallback store. Clients operating without Redis connectivity cannot rate-limit locally.
Memory grows with limit Each key uses a Redis sorted set with up to limit members. High limit values (e.g., 1M req/day) may increase Redis memory usage; consider using a separate, smaller limiter for high-frequency use cases.
Clock skew In multi-node deployments, clock skew between application nodes can cause slight inaccuracies in the window boundary. Using a Redis server time (TIME command) would eliminate this but would require an extra round-trip.
No retry-after header The ResetAfter field in Result is set to the full window duration. A precise "retry after N seconds" value would require tracking the oldest entry's timestamp.
Single Redis instance Production deployments should consider Redis Sentinel or Redis Cluster for high availability.

📄 License

Distributed under the MIT License. See LICENSE for more information.

Documentation

Overview

Package ratelimiter provides a distributed rate limiter for Go using Redis and the Sliding Window Counter algorithm.

This package is designed for high-performance, concurrent systems and ensures atomic rate-limiting operations across multiple instances by leveraging Redis Lua scripting.

Typical use cases include:

  • API rate limiting
  • Distributed systems throttling
  • Per-user or per-IP request control

See the README for integration examples and framework-specific middleware.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Config

type Config struct {
	Limit  int           // Maximum number of allowed requests.
	Window time.Duration // The duration of the sliding window.
}

Config holds the parameters for the rate limiting window.

type Limiter

type Limiter interface {
	// Allow checks if a single request for the given key is permitted within the configured
	// time window. Equivalent to AllowN(ctx, key, 1).
	// Returns a Result containing the current status or an error if the store is unreachable.
	Allow(ctx context.Context, key string) (*Result, error)

	// AllowN checks if n units can be consumed for the given key within the configured
	// time window. Useful for weighted requests (e.g., bulk operations).
	// Returns a Result containing the current status or an error if the store is unreachable.
	AllowN(ctx context.Context, key string, n int) (*Result, error)

	// Status returns the current rate limit state for the given key without consuming
	// any units. This is a read-only "peek" operation useful for checking remaining
	// quota before sending large or expensive requests.
	Status(ctx context.Context, key string) (*Result, error)
}

Limiter defines the contract for rate limiting implementations. It allows for different backend stores (Redis, In-Memory, etc.) to be used interchangeably.

type Option

type Option func(*Config)

Option is a functional configuration for the RedisStore.

func WithLimit

func WithLimit(limit int) Option

WithLimit sets the maximum number of requests allowed within the window.

func WithWindow

func WithWindow(window time.Duration) Option

WithWindow sets the duration for the rate limiting sliding window.

type RedisStore

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

RedisStore implements the Limiter interface using Redis as the backend. It utilizes Lua scripting to ensure atomic increments and window management.

func NewRedisStore

func NewRedisStore(client *redis.Client, opts ...Option) *RedisStore

NewRedisStore initializes a new RedisStore with the provided Redis client and options. If no options are provided, it defaults to a limit of 100 requests per minute.

func (*RedisStore) Allow

func (s *RedisStore) Allow(ctx context.Context, key string) (*Result, error)

Allow checks if a single request (1 unit) is permitted for the given key. It delegates to AllowN with n=1, maintaining full backward compatibility.

func (*RedisStore) AllowN added in v1.1.0

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

AllowN checks if n units can be consumed for the given key within the configured time window. The operation is atomic via Lua scripting and safe for distributed use.

func (*RedisStore) Status added in v1.1.0

func (s *RedisStore) Status(ctx context.Context, key string) (*Result, error)

Status returns the current rate limit state for the given key without consuming any units. This is a lightweight "peek" useful for checking quota before expensive ops. The Allowed field is always true since no units are consumed; callers should check Remaining.

type Result

type Result struct {
	Allowed    bool          // True if the request is permitted.
	Remaining  int64         // Number of units remaining in the current window.
	Limit      int           // The total configured limit for the window.
	ResetAfter time.Duration // Time remaining until the rate limit window resets.
}

Result represents the outcome of a rate limit check.

Directories

Path Synopsis
examples
gin command
middleware
gin

Jump to

Keyboard shortcuts

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