goratelimit

package module
v1.3.0 Latest Latest
Warning

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

Go to latest
Published: Mar 3, 2026 License: MIT Imports: 11 Imported by: 0

README

go-ratelimit

CI Go Reference Go Report Card

Rate limiting for Go that stays out of your way. One import. Seven algorithms. Any backend. Drop into any framework in three lines.

limiter, _ := goratelimit.New("redis://localhost:6379", goratelimit.PerMinute(100))
r.Use(middleware.RateLimit(limiter, middleware.KeyByIP))

That's it. You're production-ready.


Why this one

Most Go rate limiters make you choose between simple-but-wrong and powerful-but-painful. This library gives you both — sensible defaults that work instantly, and the knobs to tune every detail when you need them.

  • Seven algorithms — from Fixed Window to GCRA to Count-Min Sketch, each the right tool for a specific problem. Pick one or chain them.
  • Any backend — in-memory for tests, Redis standalone, Cluster, Sentinel, or Ring for production. Same API, zero code changes.
  • Every major framework — net/http, Gin, Echo, Fiber, gRPC. Copy one line.
  • Built for DDoS — Count-Min Sketch absorbs billion-key attack traffic in 30KB of fixed memory. PreFilter chains it with GCRA so attackers never reach Redis.
  • Honest benchmarks — 86k req/sec under 1000 concurrent VUs, p99 under 11ms, correctness verified 10,000+ times. See the numbers.

Install

go get github.com/krishna-kudari/ratelimit

Requires Go 1.21+. No CGO. No system dependencies.


Start in 2 minutes

In-memory (perfect for development and tests)
import goratelimit "github.com/krishna-kudari/ratelimit"

limiter, _ := goratelimit.NewInMemory(goratelimit.PerMinute(100))

result, _ := limiter.Allow(ctx, "user:123")
if !result.Allowed {
    // result.RetryAfter tells the client exactly when to try again
}
With Redis (production)
limiter, _ := goratelimit.New("redis://localhost:6379", goratelimit.PerMinute(100))

Switch from in-memory to Redis by changing one string. Your application code doesn't change.

As HTTP middleware
import "github.com/krishna-kudari/ratelimit/middleware"

mux.Handle("/api/", middleware.RateLimit(limiter, middleware.KeyByIP)(handler))

Every response automatically gets the headers your clients expect:

X-RateLimit-Limit:     100
X-RateLimit-Remaining: 73
X-RateLimit-Reset:     1709391845
Retry-After:           47          (only on 429)

Algorithms

Not sure which to pick? Start with GCRA. It's what Stripe and GitHub use.

Algorithm Best for Memory Burst
Fixed Window Simple quotas, billing tiers O(1) Hard cliff
Sliding Window Log Strict per-user limits, low traffic O(n) None
Sliding Window Counter High-scale APIs, ~1% error acceptable O(1) None
Token Bucket Network throttling, SDKs O(1) ✓ Smooth
Leaky Bucket Traffic shaping, steady output O(1) Queued
GCRA API rate limiting, SaaS O(1) ✓ Configurable
Count-Min Sketch DDoS mitigation, billion-key traffic Fixed None
Fixed Window

Counts requests in a fixed time window. Resets at the boundary.

limiter, _ := goratelimit.NewFixedWindow(
    100,  // max requests
    60,   // per 60 seconds
)

Simple and fast. Watch out for burst at window boundaries — a client can fire 100 requests at 11:59 and 100 more at 12:00. Use Sliding Window Counter if that matters to you.

Sliding Window Log

Stores every request timestamp. Perfectly accurate, no boundary bursts.

limiter, _ := goratelimit.NewSlidingWindow(100, 60)

The right choice when you're billing per request and need exact counts. Memory grows with traffic — not suitable for high-cardinality keys at scale.

Sliding Window Counter

Approximates a sliding window using two fixed windows. ~1% worst-case error. O(1) memory. This is what Cloudflare uses.

limiter, _ := goratelimit.NewSlidingWindowCounter(100, 60)
Token Bucket

Tokens refill at a steady rate. Each request costs one token. Leftover tokens accumulate as burst capacity.

limiter, _ := goratelimit.NewTokenBucket(
    100,  // capacity (max burst)
    10,   // refill rate per second
)

The right choice when bursts are intentional — mobile clients syncing after a gap, batch jobs, anything that idles and then fires.

Leaky Bucket

Requests fill a bucket that leaks at a constant rate. Two modes:

// Policing — excess requests are dropped immediately (fast 429)
limiter, _ := goratelimit.NewLeakyBucket(100, 10, goratelimit.Policing)

// Shaping — excess requests are queued (smooth output, adds delay)
limiter, _ := goratelimit.NewLeakyBucket(100, 10, goratelimit.Shaping)

Use Policing for APIs. Use Shaping when you control both sides and want to smooth traffic rather than reject it.

GCRA

Generic Cell Rate Algorithm. Single timestamp per key, exact accounting, configurable burst. What Stripe, GitHub, and Shopify use.

limiter, _ := goratelimit.NewGCRA(
    16,  // sustained rate (requests per second)
    32,  // burst allowance
)

If you only learn one algorithm, learn this one. O(1) memory, exact counts, handles burst as a first-class concept.

Count-Min Sketch

A probabilistic data structure that tracks request counts in fixed memory, regardless of how many unique keys exist.

limiter, _ := goratelimit.NewCMS(
    100,   // requests per window
    60,    // window seconds
    0.01,  // 1% error rate
    0.001, // 0.1% failure probability
)

fmt.Println(goratelimit.CMSMemoryBytes(0.01, 0.001)) // 30,464 bytes — fixed forever

The math: a 30KB array replaces a map that would grow to 50GB under a billion-key DDoS attack. Counts are approximate — always slightly over, never under. At DDoS scale, a 1% overcount is an acceptable tradeoff for a 1,000,000x memory reduction.

The right choice when you can't bound the number of unique keys.


Chaining Algorithms — PreFilter

Chain a fast local CMS with a precise distributed limiter. Attack traffic gets absorbed in nanoseconds at the CMS layer. Legitimate traffic — the small fraction that looks normal — passes through to GCRA on Redis.

// Fast local sketch — no network calls, no Redis load during attacks
cms, _ := goratelimit.NewCMS(100, 60, 0.01, 0.001)

// Precise distributed limiter for traffic that passes the sketch
gcra, _ := goratelimit.NewGCRA(16, 32, goratelimit.WithRedis(client))

// Chain them — CMS runs first, GCRA only sees what CMS lets through
limiter := goratelimit.NewPreFilter(cms, gcra)

Under a billion-IP DDoS, Redis sees almost nothing. Your API stays up.


Middleware

net/http
import "github.com/krishna-kudari/ratelimit/middleware"

mux.Handle("/api/", middleware.RateLimit(limiter, middleware.KeyByIP)(handler))
Gin
import "github.com/krishna-kudari/ratelimit/middleware/ginmw"

r.Use(ginmw.RateLimit(limiter, ginmw.KeyByClientIP))
Echo
import "github.com/krishna-kudari/ratelimit/middleware/echomw"

e.Use(echomw.RateLimit(limiter, echomw.KeyByRealIP))
Fiber
import "github.com/krishna-kudari/ratelimit/middleware/fibermw"

app.Use(fibermw.RateLimit(limiter, fibermw.KeyByIP))
gRPC
import "github.com/krishna-kudari/ratelimit/middleware/grpcmw"

grpc.ChainUnaryInterceptor(grpcmw.UnaryServerInterceptor(limiter, grpcmw.KeyByPeer))
grpc.ChainStreamInterceptor(grpcmw.StreamServerInterceptor(limiter, grpcmw.StreamKeyByPeer))
Key extractors — built-in
middleware.KeyByIP          // client IP
middleware.KeyByRealIP      // X-Forwarded-For aware, for proxied traffic
middleware.KeyByAPIKey      // Authorization header
Key extractors — custom
// Per-tenant + per-route limiting — most SaaS APIs need exactly this
middleware.RateLimit(limiter, func(r *http.Request) string {
    return r.Header.Get("X-Tenant-ID") + ":" + r.URL.Path
})

Advanced

Different limits per plan

The most common real-world need — free, pro, and enterprise tiers with different limits.

limiter, _ := goratelimit.NewFixedWindow(60, 60,
    goratelimit.WithLimitFunc(func(ctx context.Context, key string) int64 {
        switch getPlan(ctx, key) {
        case "pro":         return 1_000
        case "enterprise":  return 100_000
        default:            return 60    // free tier
        }
    }),
)
L1 + L2 cache — skip Redis on the hot path
import "github.com/krishna-kudari/ratelimit/cache"

// Checks in-process cache first. Only hits Redis on a miss.
// L1 hit: ~100ns. L2 Redis hit: ~1ms.
cached := cache.New(limiter, cache.WithTTL(100*time.Millisecond))
Prometheus metrics
import "github.com/krishna-kudari/ratelimit/metrics"

collector := metrics.NewCollector()
limiter = metrics.Wrap(limiter, metrics.GCRA, collector)

// Automatically exposes:
// ratelimit_requests_total{algorithm="gcra", result="allowed|denied"}
// ratelimit_request_duration_seconds{quantile="0.5|0.95|0.99"}
Redis Cluster
limiter, _ := goratelimit.NewGCRA(100, 20,
    goratelimit.WithRedis(clusterClient),
    goratelimit.WithHashTag(), // keys become {user:123} for correct slot routing
)
Fail-open vs fail-closed
goratelimit.WithFailOpen(true)  // allow requests if Redis is down (default)
goratelimit.WithFailOpen(false) // deny requests if Redis is down

Pick based on your threat model. Public APIs usually fail open — a Redis blip shouldn't take down your service. Internal or security-critical APIs fail closed.

Builder API — when you want everything explicit
limiter, _ := goratelimit.NewBuilder().
    SlidingWindowCounter(100, 60*time.Second).
    Redis(client).
    HashTag().
    Build()

Benchmarks

Microbenchmarks — algorithm cost in isolation

Single goroutine, in-memory, no network, 10 runs each.

go test -bench=. -benchmem -count=10 ./...
Algorithm ops/sec ns/op allocs/op
GCRA 17,200,000 57.7 1
Fixed Window 17,000,000 59.3 1
Token Bucket 14,500,000 69.0 1
Count-Min Sketch 10,600,000 94.5 1

Apple M4 · Go 1.23 · in-process memory store

The 1 allocs/op is the *Result struct per call. Tracked as a known improvement — eliminating it would push GCRA to ~40 ns/op.

Load tests — real concurrent pressure

1000 goroutines hammering a real HTTP server simultaneously for 60 seconds. This is what your users actually experience.

./bench/run_load.sh              # all 5 algorithms
python3 bench/parse_summaries.py # summary table
Algorithm req/sec p50 p95 p99 rate limited
GCRA 86,559 1.19ms 6.83ms 10.24ms 80.94%
Token Bucket 86,024 1.14ms 7.03ms 10.83ms 80.82%
PreFilter 80,408 1.13ms 6.95ms 10.79ms 97.01%†
Fixed Window 78,926 1.23ms 7.11ms 11.19ms 78.88%
Count-Min Sketch 78,910 1.22ms 7.17ms 11.50ms 89.60%

Apple M4 · k6 · 1000 VUs · in-memory store · 1000 unique API keys

† PreFilter stacks CMS and GCRA limits by design. It's intended for DDoS scenarios where blocking aggressively is the goal.

How latency scales with concurrency (GCRA)
VUs     p50       p99
────────────────────────
50      0.21ms    1.2ms
100     0.31ms    2.1ms
250     0.58ms    4.8ms
500     0.89ms    7.4ms
1000    1.19ms   10.24ms

p99 grows sub-linearly. At 20x more concurrent users, p99 grows roughly 8x — not 20x. The algorithm doesn't degrade sharply under pressure.

The gap between ns/op and p99 is real and expected

The microbenchmark (57 ns/op) measures one goroutine with no contention. The load test p99 (10ms) measures 1000 goroutines contending on the same mutex. Both numbers are true. The difference is the cost of correctness under real concurrent pressure — and 10ms p99 at 86k req/sec on a laptop is a number worth putting in production.

Correctness under concurrency

Speed without correctness is useless for a rate limiter.

500 goroutines fire simultaneously against a limit of 100. Exactly 100 must be allowed — not 99, not 101.

go test -run TestCorrectness -v -count=100 ./...

This test has passed 10,000+ consecutive runs in CI. If your atomicity is broken — a missing lock, a race in your Lua script, a TOCTOU — this test will catch it.


Examples

Example What it shows
basic All 7 algorithms, AllowN, Reset, Builder
httpserver net/http middleware
ginserver Gin middleware
echoserver Echo middleware
fiberserver Fiber middleware
grpcserver gRPC unary + stream interceptors
redis Redis backend, Cluster, hash tags
advanced Dynamic limits, L1 cache, Prometheus, PreFilter
demo Interactive browser visualizer for all algorithms
Interactive demo

See every algorithm in action — configurable parameters, burst testing, real-time visualization. No Redis required.

cd examples/demo && go run .
# open http://localhost:8080

Full API reference

Constructors
// Auto-selects in-memory or Redis based on URL
New(redisURL string, rate Rate, opts ...Option) (Limiter, error)
NewInMemory(rate Rate, opts ...Option) (Limiter, error)

// Algorithm-specific
NewFixedWindow(maxRequests, windowSeconds int64, opts ...Option) (Limiter, error)
NewSlidingWindow(maxRequests, windowSeconds int64, opts ...Option) (Limiter, error)
NewSlidingWindowCounter(maxRequests, windowSeconds int64, opts ...Option) (Limiter, error)
NewTokenBucket(capacity, refillRate int64, opts ...Option) (Limiter, error)
NewLeakyBucket(capacity, leakRate int64, mode LeakyBucketMode, opts ...Option) (Limiter, error)
NewGCRA(rate, burst int64, opts ...Option) (Limiter, error)
NewCMS(limit, windowSeconds int64, epsilon, delta float64, opts ...Option) (Limiter, error)
NewPreFilter(local, precise Limiter) Limiter

// Builder
NewBuilder() *Builder
CMSMemoryBytes(epsilon, delta float64) int
Rate helpers
PerSecond(n int64) Rate
PerMinute(n int64) Rate
PerHour(n int64) Rate
Limiter interface
type Limiter interface {
    Allow(ctx context.Context, key string) (*Result, error)
    AllowN(ctx context.Context, key string, n int) (*Result, error)
    Reset(ctx context.Context, key string) error
}
Result
type Result struct {
    Allowed    bool
    Remaining  int64
    Limit      int64
    ResetAt    time.Time
    RetryAfter time.Duration  // how long to wait before retrying (only meaningful when !Allowed)
}
Options
Option Description Default
WithRedis(client) Redis backing store in-memory
WithStore(store) Custom store.Store implementation
WithKeyPrefix(s) Redis key prefix "ratelimit"
WithFailOpen(bool) Allow requests on backend error true
WithHashTag() Wrap keys for Redis Cluster slot routing off
WithLimitFunc(fn) Dynamic per-key limit resolver

License

MIT — do whatever you want with it.


Built with care. Benchmarked honestly. Correctness verified.
If it saves you time, consider starring the repo or opening a PR.

Documentation

Overview

Package goratelimit provides production-grade rate limiting for Go with six algorithms, in-memory and Redis backends, and drop-in middleware for net/http, Gin, Echo, Fiber, and gRPC.

Algorithms

  • Fixed Window Counter — simple, fixed time intervals
  • Sliding Window Log — precise, stores every timestamp
  • Sliding Window Counter — weighted approximation, O(1) memory
  • Token Bucket — steady refill, burst-friendly
  • Leaky Bucket — constant drain, policing or shaping mode
  • GCRA — virtual scheduling with sustained rate + burst
  • Count-Min Sketch — fixed-memory probabilistic pre-filter

Quick Start

limiter, err := goratelimit.New("", goratelimit.PerMinute(100))
result, _ := limiter.Allow(ctx, "user:123")
if result.Allowed {
    // serve request
}

With Redis

limiter, _ := goratelimit.New("redis://localhost:6379", goratelimit.PerMinute(100))

Algorithm-Specific Constructors

limiter, _ := goratelimit.NewTokenBucket(100, 10,
    goratelimit.WithRedis(redisClient),
)

Builder API

limiter, _ := goratelimit.NewBuilder().
    SlidingWindowCounter(100, 60*time.Second).
    Redis(client).
    Build()

All algorithms implement the Limiter interface and return a Result with Allowed, Remaining, Limit, ResetAt, and RetryAfter fields.

Index

Examples

Constants

View Source
const Unlimited int64 = -1

Unlimited is the sentinel value for no rate limit. Return it from LimitFunc to allow the key without consuming quota (e.g. trusted users, internal services).

Variables

This section is empty.

Functions

func CMSMemoryBytes added in v1.1.0

func CMSMemoryBytes(epsilon, delta float64) int

CMSMemoryBytes returns the approximate heap usage of a CMS limiter created with the given error parameters. Useful for capacity planning without constructing a limiter.

Example
package main

import (
	"fmt"

	goratelimit "github.com/krishna-kudari/ratelimit"
)

func main() {
	bytes := goratelimit.CMSMemoryBytes(0.01, 0.001)
	fmt.Printf("memory=%d bytes\n", bytes)
}
Output:
memory=30464 bytes

Types

type Builder

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

Builder provides a fluent API for constructing a Limiter.

limiter, err := goratelimit.NewBuilder().
    FixedWindow(100, 60*time.Second).
    Redis(client).
    HashTag().
    Build()

func NewBuilder

func NewBuilder() *Builder

NewBuilder returns a new Builder with default options.

Example
package main

import (
	"context"
	"fmt"
	"time"

	goratelimit "github.com/krishna-kudari/ratelimit"
)

func main() {
	limiter, _ := goratelimit.NewBuilder().
		SlidingWindowCounter(100, 60*time.Second).
		KeyPrefix("api").
		FailOpen(true).
		Build()

	result, _ := limiter.Allow(context.Background(), "user:123")
	fmt.Printf("allowed=%v remaining=%d\n", result.Allowed, result.Remaining)
}
Output:
allowed=true remaining=99

func (*Builder) Build

func (b *Builder) Build() (Limiter, error)

Build validates the configuration and returns the configured Limiter.

func (*Builder) CMS added in v1.1.0

func (b *Builder) CMS(limit int64, window time.Duration, epsilon, delta float64) *Builder

CMS configures a Count-Min Sketch rate limiter. limit is the max requests per window. window is the window duration. epsilon is the acceptable error rate (e.g. 0.01). delta is the failure probability (e.g. 0.001). This algorithm is in-memory only; Redis options are ignored.

func (*Builder) DryRun added in v1.3.0

func (b *Builder) DryRun(dryRun bool) *Builder

DryRun enables dry-run mode: never deny, log when a request would have been denied.

func (*Builder) DryRunLogFunc added in v1.3.0

func (b *Builder) DryRunLogFunc(fn func(key string, result *Result)) *Builder

DryRunLogFunc sets the logger called when dry run would have denied a request.

func (*Builder) FailOpen

func (b *Builder) FailOpen(v bool) *Builder

FailOpen sets the fail-open/fail-closed behavior when the backend is unreachable.

func (*Builder) FixedWindow

func (b *Builder) FixedWindow(maxRequests int64, window time.Duration) *Builder

FixedWindow configures a Fixed Window algorithm. maxRequests is the limit per window. window is the window duration.

func (*Builder) GCRA

func (b *Builder) GCRA(rate, burst int64) *Builder

GCRA configures a Generic Cell Rate Algorithm limiter. rate is sustained requests per second. burst is the maximum burst.

func (*Builder) HashTag

func (b *Builder) HashTag() *Builder

HashTag enables Redis Cluster hash-tag wrapping on keys.

func (*Builder) KeyPrefix

func (b *Builder) KeyPrefix(prefix string) *Builder

KeyPrefix sets the prefix prepended to all storage keys.

func (*Builder) LeakyBucket

func (b *Builder) LeakyBucket(capacity, leakRate int64, mode LeakyBucketMode) *Builder

LeakyBucket configures a Leaky Bucket algorithm. capacity is the bucket size. leakRate is tokens leaked per second. mode selects Policing (hard reject) or Shaping (queue with delay).

func (*Builder) LimitFunc

func (b *Builder) LimitFunc(fn func(ctx context.Context, key string) int64) *Builder

LimitFunc sets a dynamic per-key limit resolver. The function is called on every Allow/AllowN with context and key. Return the limit, goratelimit.Unlimited for no limit, or <= 0 to use the default.

func (*Builder) OnLimitExceeded added in v1.3.0

func (b *Builder) OnLimitExceeded(fn func(ctx context.Context, key string, result *Result)) *Builder

OnLimitExceeded sets a callback invoked when a request is denied due to rate limit. Use for alerting, analytics, or logging. Not called on backend errors or when DryRun is true.

func (*Builder) Redis

func (b *Builder) Redis(client redis.UniversalClient) *Builder

Redis sets the Redis backend. Accepts any redis.UniversalClient.

func (*Builder) SlidingWindow

func (b *Builder) SlidingWindow(maxRequests int64, window time.Duration) *Builder

SlidingWindow configures a Sliding Window Log algorithm. maxRequests is the limit per window. window is the window duration. Stores every request timestamp; for high throughput prefer SlidingWindowCounter.

func (*Builder) SlidingWindowCounter

func (b *Builder) SlidingWindowCounter(maxRequests int64, window time.Duration) *Builder

SlidingWindowCounter configures a Sliding Window Counter algorithm. maxRequests is the limit per window. window is the window duration. Uses weighted-counter approximation with O(1) memory per key.

func (*Builder) Store

func (b *Builder) Store(s store.Store) *Builder

Store sets a custom store.Store backend.

func (*Builder) TokenBucket

func (b *Builder) TokenBucket(capacity, refillRate int64) *Builder

TokenBucket configures a Token Bucket algorithm. capacity is the burst size. refillRate is tokens added per second.

type Clock added in v1.3.0

type Clock interface {
	Now() time.Time
}

Clock provides the current time. Use in production (nil = time.Now) or inject a fake clock in tests to advance time without time.Sleep.

type FakeClock added in v1.3.0

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

FakeClock is a deterministic clock for testing. Advance time with Advance instead of sleeping.

func NewFakeClock added in v1.3.0

func NewFakeClock() *FakeClock

NewFakeClock returns a fake clock starting at the Unix epoch. Use Advance to move time forward in tests.

func NewFakeClockAt added in v1.3.0

func NewFakeClockAt(t time.Time) *FakeClock

NewFakeClockAt returns a fake clock starting at the given time.

func (*FakeClock) Advance added in v1.3.0

func (c *FakeClock) Advance(d time.Duration)

Advance moves the clock forward by d. Use in tests to simulate elapsed time without time.Sleep, e.g. clock.Advance(61 * time.Second) to expire a 60s window.

func (*FakeClock) Now added in v1.3.0

func (c *FakeClock) Now() time.Time

Now returns the current fake time.

type LeakyBucketMode

type LeakyBucketMode string

LeakyBucketMode defines the operating mode of a leaky bucket limiter.

const (
	// Policing mode drops requests that exceed capacity (hard rejection).
	Policing LeakyBucketMode = "policing"
	// Shaping mode queues requests and assigns a processing delay.
	Shaping LeakyBucketMode = "shaping"
)

type LeakyBucketResult

type LeakyBucketResult struct {
	Result
	Delay time.Duration // For shaping mode: how long to wait before processing.
}

LeakyBucketResult extends Result with shaping-specific delay information.

type Limiter

type Limiter interface {
	// Allow checks whether a single request identified by key should be allowed.
	Allow(ctx context.Context, key string) (Result, error)

	// AllowN checks whether n requests identified by key should be allowed.
	AllowN(ctx context.Context, key string, n int) (Result, error)

	// Reset clears all rate limit state for the given key.
	Reset(ctx context.Context, key string) error
}

Limiter is the core interface for all rate limiting algorithms. All implementations (in-memory and Redis-backed) satisfy this interface, making algorithms swappable without changing caller code.

Example (AllowN)
package main

import (
	"context"
	"fmt"

	goratelimit "github.com/krishna-kudari/ratelimit"
)

func main() {
	limiter, _ := goratelimit.NewTokenBucket(10, 1)
	result, _ := limiter.AllowN(context.Background(), "user:123", 3)
	fmt.Printf("allowed=%v remaining=%d\n", result.Allowed, result.Remaining)
}
Output:
allowed=true remaining=7
Example (Reset)
package main

import (
	"context"
	"fmt"

	goratelimit "github.com/krishna-kudari/ratelimit"
)

func main() {
	ctx := context.Background()
	limiter, _ := goratelimit.NewFixedWindow(1, 60)
	limiter.Allow(ctx, "user:123")

	result, _ := limiter.Allow(ctx, "user:123")
	fmt.Printf("before reset: allowed=%v\n", result.Allowed)

	_ = limiter.Reset(ctx, "user:123")
	result, _ = limiter.Allow(ctx, "user:123")
	fmt.Printf("after reset:  allowed=%v\n", result.Allowed)
}
Output:
before reset: allowed=false
after reset:  allowed=true

func New added in v1.2.0

func New(redisURL string, rate Rate, opts ...Option) (Limiter, error)

New creates a rate limiter with sensible defaults (Fixed Window algorithm). Pass an empty redisURL for in-memory mode, or a Redis URL (e.g. "redis://localhost:6379/0") for distributed mode.

limiter, err := goratelimit.New("", goratelimit.PerMinute(100))

limiter, err := goratelimit.New("redis://localhost:6379", goratelimit.PerMinute(100))
Example
package main

import (
	"context"
	"fmt"

	goratelimit "github.com/krishna-kudari/ratelimit"
)

func main() {
	limiter, _ := goratelimit.New("", goratelimit.PerMinute(100))
	result, _ := limiter.Allow(context.Background(), "user:123")
	fmt.Printf("allowed=%v remaining=%d limit=%d\n", result.Allowed, result.Remaining, result.Limit)
}
Output:
allowed=true remaining=99 limit=100

func NewCMS added in v1.1.0

func NewCMS(limit, windowSeconds int64, epsilon, delta float64, opts ...Option) (Limiter, error)

NewCMS creates a Count-Min Sketch rate limiter that uses fixed memory regardless of the number of unique keys. It approximates per-key counts using two rotating sketches for a sliding window effect.

This is an in-memory-only algorithm — no Redis backend is supported. Its primary use case is as a fast local pre-filter in front of a precise distributed limiter (see NewPreFilter).

limit         — max requests per window
windowSeconds — window size in seconds
epsilon       — acceptable error rate (e.g. 0.01 = 1 %)
delta         — failure probability  (e.g. 0.001 = 0.1 %)

Memory: 2 × ⌈e/ε⌉ × ⌈ln(1/δ)⌉ × 8 bytes. Example: ε=0.01, δ=0.001 → ~30 KB fixed.

Example
package main

import (
	"context"
	"fmt"

	goratelimit "github.com/krishna-kudari/ratelimit"
)

func main() {
	limiter, _ := goratelimit.NewCMS(100, 60, 0.01, 0.001)
	result, _ := limiter.Allow(context.Background(), "user:123")
	fmt.Printf("allowed=%v remaining=%d\n", result.Allowed, result.Remaining)
}
Output:
allowed=true remaining=99

func NewFixedWindow

func NewFixedWindow(maxRequests, windowSeconds int64, opts ...Option) (Limiter, error)

NewFixedWindow creates a Fixed Window rate limiter. maxRequests is the maximum requests allowed per window. windowSeconds is the window duration in seconds. Pass WithRedis for distributed mode; omit for in-memory.

Example
package main

import (
	"context"
	"fmt"

	goratelimit "github.com/krishna-kudari/ratelimit"
)

func main() {
	limiter, _ := goratelimit.NewFixedWindow(10, 60)
	result, _ := limiter.Allow(context.Background(), "user:123")
	fmt.Printf("allowed=%v remaining=%d\n", result.Allowed, result.Remaining)
}
Output:
allowed=true remaining=9

func NewGCRA

func NewGCRA(rate, burst int64, opts ...Option) (Limiter, error)

NewGCRA creates a GCRA (Generic Cell Rate Algorithm) rate limiter. rate is the sustained request rate per second. burst is the maximum burst size. Pass WithRedis for distributed mode; omit for in-memory.

Example
package main

import (
	"context"
	"fmt"

	goratelimit "github.com/krishna-kudari/ratelimit"
)

func main() {
	limiter, _ := goratelimit.NewGCRA(5, 10)
	result, _ := limiter.Allow(context.Background(), "user:123")
	fmt.Printf("allowed=%v remaining=%d\n", result.Allowed, result.Remaining)
}
Output:
allowed=true remaining=8

func NewInMemory added in v1.2.0

func NewInMemory(rate Rate, opts ...Option) (Limiter, error)

NewInMemory creates an in-memory rate limiter — ideal for tests and single-process deployments.

limiter, err := goratelimit.NewInMemory(goratelimit.PerMinute(100))
Example
package main

import (
	"context"
	"fmt"

	goratelimit "github.com/krishna-kudari/ratelimit"
)

func main() {
	limiter, _ := goratelimit.NewInMemory(goratelimit.PerHour(500))
	result, _ := limiter.Allow(context.Background(), "user:123")
	fmt.Printf("allowed=%v limit=%d\n", result.Allowed, result.Limit)
}
Output:
allowed=true limit=500

func NewLeakyBucket

func NewLeakyBucket(capacity, leakRate int64, mode LeakyBucketMode, opts ...Option) (Limiter, error)

NewLeakyBucket creates a Leaky Bucket rate limiter. capacity is the bucket size. leakRate is tokens leaked per second. mode selects Policing (hard reject) or Shaping (queue with delay). Pass WithRedis for distributed mode; omit for in-memory.

Example (Policing)
package main

import (
	"context"
	"fmt"

	goratelimit "github.com/krishna-kudari/ratelimit"
)

func main() {
	limiter, _ := goratelimit.NewLeakyBucket(10, 1, goratelimit.Policing)
	result, _ := limiter.Allow(context.Background(), "user:123")
	fmt.Printf("allowed=%v remaining=%d\n", result.Allowed, result.Remaining)
}
Output:
allowed=true remaining=9
Example (Shaping)
package main

import (
	"context"
	"fmt"

	goratelimit "github.com/krishna-kudari/ratelimit"
)

func main() {
	limiter, _ := goratelimit.NewLeakyBucket(10, 1, goratelimit.Shaping)
	result, _ := limiter.Allow(context.Background(), "user:123")
	fmt.Printf("allowed=%v\n", result.Allowed)
}
Output:
allowed=true

func NewPreFilter added in v1.1.0

func NewPreFilter(local, precise Limiter) Limiter

NewPreFilter creates a rate limiter that checks the local limiter first and only escalates to the precise limiter when the local check passes.

Under normal traffic both limiters are consulted and the precise limiter's result is authoritative. Under attack the local limiter absorbs the load, shielding the remote backend from being overwhelmed.

cms, _  := goratelimit.NewCMS(100, 60, 0.01, 0.001)
gcra, _ := goratelimit.NewGCRA(10, 20, goratelimit.WithRedis(client))
limiter := goratelimit.NewPreFilter(cms, gcra)
Example
package main

import (
	"context"
	"fmt"

	goratelimit "github.com/krishna-kudari/ratelimit"
)

func main() {
	local, _ := goratelimit.NewCMS(100, 60, 0.01, 0.001)
	precise, _ := goratelimit.NewGCRA(5, 10)
	limiter := goratelimit.NewPreFilter(local, precise)

	result, _ := limiter.Allow(context.Background(), "user:123")
	fmt.Printf("allowed=%v remaining=%d\n", result.Allowed, result.Remaining)
}
Output:
allowed=true remaining=8

func NewSlidingWindow

func NewSlidingWindow(maxRequests, windowSeconds int64, opts ...Option) (Limiter, error)

NewSlidingWindow creates a Sliding Window Log rate limiter. maxRequests is the maximum requests allowed per window. windowSeconds is the window duration in seconds. Note: this algorithm stores every request timestamp and has O(n) memory per key. For high-throughput keys, prefer NewSlidingWindowCounter. Pass WithRedis for distributed mode; omit for in-memory.

Example
package main

import (
	"context"
	"fmt"

	goratelimit "github.com/krishna-kudari/ratelimit"
)

func main() {
	limiter, _ := goratelimit.NewSlidingWindow(10, 60)
	result, _ := limiter.Allow(context.Background(), "user:123")
	fmt.Printf("allowed=%v remaining=%d\n", result.Allowed, result.Remaining)
}
Output:
allowed=true remaining=9

func NewSlidingWindowCounter

func NewSlidingWindowCounter(maxRequests, windowSeconds int64, opts ...Option) (Limiter, error)

NewSlidingWindowCounter creates a Sliding Window Counter rate limiter. This uses the weighted-counter approximation (~1% error) with O(1) memory per key. maxRequests is the maximum requests allowed per window. windowSeconds is the window duration in seconds. Pass WithRedis for distributed mode; omit for in-memory.

Example
package main

import (
	"context"
	"fmt"

	goratelimit "github.com/krishna-kudari/ratelimit"
)

func main() {
	limiter, _ := goratelimit.NewSlidingWindowCounter(10, 60)
	result, _ := limiter.Allow(context.Background(), "user:123")
	fmt.Printf("allowed=%v remaining=%d\n", result.Allowed, result.Remaining)
}
Output:
allowed=true remaining=9

func NewTokenBucket

func NewTokenBucket(capacity, refillRate int64, opts ...Option) (Limiter, error)

NewTokenBucket creates a Token Bucket rate limiter. capacity is the maximum number of tokens (burst size). refillRate is the number of tokens added per second. Pass WithRedis for distributed mode; omit for in-memory.

Example
package main

import (
	"context"
	"fmt"

	goratelimit "github.com/krishna-kudari/ratelimit"
)

func main() {
	limiter, _ := goratelimit.NewTokenBucket(100, 10)
	result, _ := limiter.Allow(context.Background(), "user:123")
	fmt.Printf("allowed=%v remaining=%d\n", result.Allowed, result.Remaining)
}
Output:
allowed=true remaining=99

type Option

type Option func(*Options)

Option is a functional option for configuring a Limiter.

func WithClock added in v1.3.0

func WithClock(clock Clock) Option

WithClock sets the clock used for time. In tests, pass a FakeClock and call Advance to simulate elapsed time without time.Sleep.

Example
package main

import (
	"context"
	"fmt"
	"time"

	goratelimit "github.com/krishna-kudari/ratelimit"
)

func main() {
	clock := goratelimit.NewFakeClock()
	limiter, _ := goratelimit.NewFixedWindow(2, 60, goratelimit.WithClock(clock))

	ctx := context.Background()
	limiter.Allow(ctx, "k")
	limiter.Allow(ctx, "k")
	r, _ := limiter.Allow(ctx, "k")
	fmt.Printf("before advance: allowed=%v\n", r.Allowed)

	clock.Advance(61 * time.Second)
	r, _ = limiter.Allow(ctx, "k")
	fmt.Printf("after advance:  allowed=%v\n", r.Allowed)
}
Output:
before advance: allowed=false
after advance:  allowed=true

func WithDryRun added in v1.3.0

func WithDryRun(dryRun bool) Option

WithDryRun enables dry-run mode: the limiter never denies; when a request would have been denied, DryRunLogFunc is called (or [DRYRUN] is logged). Use for safe production rollout to observe what would be rate limited.

Example
package main

import (
	"context"
	"fmt"

	goratelimit "github.com/krishna-kudari/ratelimit"
)

func main() {
	limiter, _ := goratelimit.NewFixedWindow(2, 60, goratelimit.WithDryRun(true))
	ctx := context.Background()

	limiter.Allow(ctx, "key")
	limiter.Allow(ctx, "key")
	// Would be denied without dry run; with dry run still allowed
	r, _ := limiter.Allow(ctx, "key")
	fmt.Printf("allowed=%v (limit=%d remaining=%d)\n", r.Allowed, r.Limit, r.Remaining)
}
Output:
allowed=true (limit=2 remaining=0)

func WithDryRunLogFunc added in v1.3.0

func WithDryRunLogFunc(fn func(key string, result *Result)) Option

WithDryRunLogFunc sets the logger called when DryRun is true and a request would have been denied. If nil, log.Printf with [DRYRUN] prefix is used.

func WithFailOpen

func WithFailOpen(failOpen bool) Option

WithFailOpen controls behavior when the backend is unreachable. If true (default), requests are allowed on errors. If false, requests are denied on errors.

func WithHashTag

func WithHashTag() Option

WithHashTag enables Redis Cluster hash-tag wrapping. Keys become "prefix:{key}" so all keys for a given user route to the same Redis Cluster slot. Required for multi-key algorithms (Sliding Window Counter) in Cluster mode.

func WithKeyPrefix

func WithKeyPrefix(prefix string) Option

WithKeyPrefix sets the prefix prepended to all storage keys. Default: "ratelimit".

func WithLimitFunc

func WithLimitFunc(fn func(ctx context.Context, key string) int64) Option

WithLimitFunc sets a dynamic limit resolver. The function is called on every Allow/AllowN with the request context and key. Use context for plan-based limits (e.g. ctx.Value("plan")). Return the effective limit, Unlimited for no limit, or <= 0 (other than Unlimited) to use the construction-time default.

Example
package main

import (
	"context"
	"fmt"

	goratelimit "github.com/krishna-kudari/ratelimit"
)

func main() {
	limiter, _ := goratelimit.NewFixedWindow(5, 60,
		goratelimit.WithLimitFunc(func(ctx context.Context, key string) int64 {
			if key == "premium" {
				return 1000
			}
			return 0
		}),
	)

	ctx := context.Background()
	r1, _ := limiter.Allow(ctx, "premium")
	r2, _ := limiter.Allow(ctx, "free")
	fmt.Printf("premium: limit=%d\nfree:    limit=%d\n", r1.Limit, r2.Limit)
}
Output:
premium: limit=1000
free:    limit=5

func WithOnLimitExceeded added in v1.3.0

func WithOnLimitExceeded(fn func(ctx context.Context, key string, result *Result)) Option

WithOnLimitExceeded sets a callback invoked when a request is denied due to rate limit. Use for alerting, analytics, or logging. Not called on backend errors or when DryRun is true.

func WithRedis

func WithRedis(client redis.UniversalClient) Option

WithRedis configures the limiter to use Redis as its backing store. Accepts any redis.UniversalClient: *redis.Client (standalone), *redis.ClusterClient (cluster), *redis.Ring (ring), or sentinel. When set, the limiter operates in distributed mode.

func WithStore

func WithStore(s store.Store) Option

WithStore configures the limiter to use a custom store.Store backend. This takes precedence over WithRedis if both are set.

type Options

type Options struct {
	// Store is the pluggable backend for rate limit state.
	// Takes precedence over RedisClient if both are set.
	Store store.Store

	// RedisClient is a Redis connection for distributed rate limiting.
	// Accepts *redis.Client, *redis.ClusterClient, *redis.Ring, or any
	// redis.UniversalClient implementation.
	RedisClient redis.UniversalClient

	// KeyPrefix is prepended to all storage keys.
	// Default: "ratelimit".
	KeyPrefix string

	// FailOpen controls behavior when the backend is unreachable.
	// If true (default), requests are allowed on errors.
	// If false, requests are denied on errors.
	FailOpen bool

	// HashTag enables Redis Cluster hash-tag wrapping of user keys.
	// When true, keys are formatted as "prefix:{key}" instead of "prefix:key",
	// ensuring all keys for the same logical entity route to the same slot.
	// This is required for Sliding Window Counter (multi-key) and recommended
	// for any Redis Cluster deployment.
	HashTag bool

	// LimitFunc dynamically resolves the rate limit for each key.
	// Called with the request context (e.g. from middleware) so limits can depend on
	// user plan, JWT claims, or other context values. Returns the effective limit
	// (maxRequests / capacity / burst). Return Unlimited for no limit; return <= 0
	// (other than Unlimited) to use the construction-time default.
	LimitFunc func(ctx context.Context, key string) int64

	// Clock provides the current time. If nil, time.Now is used.
	// Inject a FakeClock in tests to advance time without time.Sleep.
	Clock Clock

	// DryRun, when true, never denies: Allow/AllowN always return Allowed=true,
	// but when a request would have been denied, the optional DryRunLogFunc is
	// called (or log.Printf with [DRYRUN] prefix if nil) so operators can see
	// what would be rate limited.
	DryRun bool

	// DryRunLogFunc is called when DryRun is true and a request would have been
	// denied. If nil, log.Printf("[DRYRUN] would deny key=...") is used.
	DryRunLogFunc func(key string, result *Result)

	// OnLimitExceeded is called when a request is denied due to rate limit.
	// Use for alerting, analytics, or logging. Not called on backend errors or in dry-run.
	OnLimitExceeded func(ctx context.Context, key string, result *Result)
}

Options configures behavior shared across all algorithm implementations.

func (*Options) FormatKey

func (o *Options) FormatKey(key string) string

FormatKey builds a storage key. With HashTag enabled the user key is wrapped in {}: "prefix:{key}" so all derived keys for the same user land on the same Redis Cluster slot.

func (*Options) FormatKeySuffix

func (o *Options) FormatKeySuffix(key, suffix string) string

FormatKeySuffix builds a storage key with an additional suffix. "prefix:{key}:suffix" (hash-tag) or "prefix:key:suffix" (plain).

type Rate added in v1.2.0

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

Rate specifies a request limit over a time window. Create one with PerSecond, PerMinute, or PerHour.

func PerHour added in v1.2.0

func PerHour(n int64) Rate

PerHour returns a Rate allowing n requests per hour.

func PerMinute added in v1.2.0

func PerMinute(n int64) Rate

PerMinute returns a Rate allowing n requests per minute.

func PerSecond added in v1.2.0

func PerSecond(n int64) Rate

PerSecond returns a Rate allowing n requests per second.

type Result

type Result struct {
	Allowed    bool
	Remaining  int64
	Limit      int64
	ResetAt    time.Time
	RetryAfter time.Duration
}

Result holds the outcome of a rate limit check.

Directories

Path Synopsis
Package cache provides an L1 in-process cache that wraps any Limiter.
Package cache provides an L1 in-process cache that wraps any Limiter.
examples
advanced command
Advanced features — dynamic limits, fail-open, local cache, Prometheus, custom keys.
Advanced features — dynamic limits, fail-open, local cache, Prometheus, custom keys.
basic command
All seven algorithms — direct usage, AllowN, Reset, PreFilter, and Builder API.
All seven algorithms — direct usage, AllowN, Reset, PreFilter, and Builder API.
demo command
echoserver command
Complete Echo server with rate limiting middleware.
Complete Echo server with rate limiting middleware.
fiberserver command
Complete Fiber server with rate limiting middleware.
Complete Fiber server with rate limiting middleware.
ginserver command
Complete Gin server with rate limiting middleware.
Complete Gin server with rate limiting middleware.
grpcserver command
gRPC server with rate limiting interceptors.
gRPC server with rate limiting interceptors.
httpserver command
Complete net/http server with rate limiting middleware.
Complete net/http server with rate limiting middleware.
redis command
Rate limiting with Redis backend — works with standalone, Cluster, Ring, Sentinel.
Rate limiting with Redis backend — works with standalone, Cluster, Ring, Sentinel.
Package metrics provides Prometheus instrumentation for rate limiters.
Package metrics provides Prometheus instrumentation for rate limiters.
This file is kept for backward-compatibility documentation.
This file is kept for backward-compatibility documentation.
echomw
Package echomw provides Echo middleware for rate limiting.
Package echomw provides Echo middleware for rate limiting.
fibermw
Package fibermw provides Fiber middleware for rate limiting.
Package fibermw provides Fiber middleware for rate limiting.
ginmw
Package ginmw provides Gin middleware for rate limiting.
Package ginmw provides Gin middleware for rate limiting.
grpcmw
Package grpcmw provides gRPC server interceptors for rate limiting.
Package grpcmw provides gRPC server interceptors for rate limiting.
Package store defines the backend storage contract for rate limiters.
Package store defines the backend storage contract for rate limiters.
memory
Package memory provides an in-memory implementation of store.Store.
Package memory provides an in-memory implementation of store.Store.
redis
Package redis provides a Redis-backed implementation of store.Store.
Package redis provides a Redis-backed implementation of store.Store.

Jump to

Keyboard shortcuts

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