fluxrate

package module
v0.0.0-...-59951be Latest Latest
Warning

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

Go to latest
Published: May 24, 2026 License: MIT Imports: 11 Imported by: 0

README

FluxRate ⚡

Go Reference Go Report Card Build Status

FluxRate is a high-performance, production-ready, distributed rate limiting library for Go and Redis. It is built for resilience, providing automatic, thread-safe in-memory fallback if your Redis cluster goes down, and recovers seamlessly when Redis is back online.

It implements 5 advanced rate limiting algorithms using optimized Lua scripts, and provides drop-in middlewares for popular Go web frameworks (Gin, Echo, net/http).


Key Features

  • 🛠️ 5 Rate Limiting Algorithms:
    • GCRA (Generic Cell Rate Algorithm): The industry standard for cell/leaky-bucket traffic shaping.
    • Token Bucket: Perfect for supporting bursty traffic patterns with constant refill rates.
    • Sliding Window Log: 100% precise sliding window rate limiting.
    • Sliding Window Counter: Extremely memory-efficient sliding window approximation.
    • Fixed Window: Simple, high-speed, atomic block-based limiter.
  • 🛡️ Resilient Local Fallback: Automatically degrades to a high-performance, thread-safe local in-memory limiter if Redis becomes unreachable. Failing-open or failing-closed options are supported.
  • 🔗 Universal Redis Compatibility: Supports single-node Redis, Redis Sentinel, and Redis Cluster.
  • 🚀 Pre-Built Middlewares: Integrated support for Gin, Echo, and standard net/http handlers.
  • 📊 Interactive CLI Traffic Simulator: An educational tool to visualize and compare rate limiters under live workloads (constant, spiky, sine wave traffic).

Architecture Flow

graph TD
    Client[Client Request] --> MW[FluxRate Middleware]
    MW --> KeyGen[Generate Client Key]
    KeyGen --> RedisCheck{Is Redis Reachable?}
    
    %% Redis Path
    RedisCheck -- Yes --> RedisLimiter[Run Optimized Lua Script]
    RedisLimiter --> RedisResult{Allowed?}
    
    %% Fallback Path
    RedisCheck -- No / Timeout --> LogWarn[Log Warning]
    LogWarn --> FallbackLimiter[Swap to Local In-Memory Limiter]
    FallbackLimiter --> FallbackResult{Allowed?}
    
    %% Decision handling
    RedisResult -- Yes --> ServeHTTP[Forward to Handler]
    RedisResult -- No --> BlockHTTP[Return HTTP 429 Too Many Requests]
    
    FallbackResult -- Yes --> ServeHTTP
    FallbackResult -- No --> BlockHTTP

    ServeHTTP --> InjectHeaders[Inject HTTP Headers: X-RateLimit-*]
    BlockHTTP --> InjectRetryHeaders[Inject Retry-After Header]

Installation

go get github.com/ayd1ndemirci/fluxrate

Quick Start

1. Basic Usage (GCRA)
package main

import (
	"context"
	"fmt"
	"time"

	"github.com/ayd1ndemirci/fluxrate"
)

func main() {
	// Connect to Redis
	rdb := fluxrate.NewRedis("localhost:6379")

	// Create a GCRA rate limiter: allows 10 requests per 10 seconds
	limiter := fluxrate.NewGCRA(
		rdb.Client,
		10,             // limit
		10*time.Second, // window size
		fluxrate.WithKeyPrefix("api:rate:"), // optional prefix
	)

	ctx := context.Background()
	key := "user_ip_127.0.0.1"

	res, err := limiter.Allow(ctx, key)
	if err != nil {
		fmt.Printf("Rate limit evaluation failed: %v\n", err)
		return
	}

	if res.Allowed {
		fmt.Printf("Request allowed. Remaining quota: %d. Reset after: %v\n", res.Remaining, res.ResetAfter)
	} else {
		fmt.Printf("Rate limit exceeded! Retry after: %v\n", res.RetryAfter)
	}
}
2. Gin Middleware Integration
package main

import (
	"github.com/ayd1ndemirci/fluxrate"
	fluxgin "github.com/ayd1ndemirci/fluxrate/middleware/gin"
	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()
	rdb := fluxrate.NewRedis("localhost:6379")

	// Token Bucket: Capacity of 20, refills 5 tokens per second
	limiter := fluxrate.NewTokenBucket(rdb.Client, 20, 5.0)

	// Rate limit based on client IP address
	keyFunc := func(c *gin.Context) string {
		return "ip:" + c.ClientIP()
	}

	r.Use(fluxgin.Handler(limiter, keyFunc))

	r.GET("/ping", func(c *gin.Context) {
		c.JSON(200, gin.H{"message": "pong"})
	})

	r.Run(":8080")
}

Middleware Response Headers

When using pre-built middlewares, responses automatically include RFC-compliant headers:

Header Description
X-RateLimit-Limit The maximum number of requests allowed in the window.
X-RateLimit-Remaining The number of remaining requests allowed within the current window.
X-RateLimit-Reset The number of seconds remaining until the rate limit resets/refills.
Retry-After (Sent on HTTP 429 only) The number of seconds the client must wait before retrying.

Interactive CLI Traffic Simulator

FluxRate includes a live traffic simulator to let you test and analyze how different algorithms shape traffic. It starts a mock local API server and simulates request loads (sine waves, spiky bursts, or constant loads) against it.

# Run simulator with GCRA and sinusoidal traffic
go run cmd/simulator/main.go --algo=gcra --traffic=sine --duration=20s

# Run simulator in memory-only mode (no Redis required)
go run cmd/simulator/main.go --algo=token_bucket --traffic=bursty

# Run simulator with a real Redis database
go run cmd/simulator/main.go --algo=sliding_window_counter --redis=localhost:6379
Dashboard Visualization

When running, the simulator renders a real-time ASCII dashboard showing allowed vs. blocked requests and latencies:

=============================================================
 FLUXRATE SIMULATOR DASHBOARD (GCRA)
=============================================================
  Target Limit   : 10 requests per 2s
  Traffic Pattern: SINE
  Elapsed Time   : 8s / 20s
-------------------------------------------------------------
  Total Requests : 124       Success Rate:  56.45%
  Allowed (.)    : 70        Blocked (X) : 54
  Errors  (E)    : 0
  Latency        : Min: 45µs | Avg: 1.25ms | Max: 15.34ms
-------------------------------------------------------------
  Traffic Flow Timeline (last 60 requests):
  [....XXXX....XXXX....X.X.X.X.X.X.X.X.X.X.X..XXXX....XXXX....]
  (. = Allowed, X = Blocked, E = Error)
=============================================================

Algorithm Performance & Comparison

Algorithm Redis Command Complexity Memory Footprint (Redis) Burst Capacity Accuracy Best Use Case
GCRA O(1) Very Low (1 Key) High (Configurable) High API Rate Limiting, Traffic Pacing
Token Bucket O(1) Low (1 Hash) High High Burst Handling API Gateways
Sliding Window Log O(log N) High (Grows with reqs) Low 100% High-value, low-frequency security operations
Sliding Window Counter O(1) Low (2 Keys) Medium Approximate Large scale web apps with strict memory targets
Fixed Window O(1) Very Low (1 Key) None Low Basic limiting where window resets are acceptable

License

This project is licensed under the MIT License - see the LICENSE file for details.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrRedisUnavailable = errors.New("fluxrate: redis server is unavailable")
	ErrInvalidLimit     = errors.New("fluxrate: limit must be greater than zero")
	ErrInvalidWindow    = errors.New("fluxrate: window duration must be greater than zero")
)

Functions

func IsRedisDown

func IsRedisDown(err error) bool

Types

type FixedWindowLimiter

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

func NewFixedWindow

func NewFixedWindow(client redis.Cmdable, limit int64, window time.Duration, opts ...Option) *FixedWindowLimiter

func (*FixedWindowLimiter) Allow

func (l *FixedWindowLimiter) Allow(ctx context.Context, key string) (*Result, error)

type GCRALimiter

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

func NewGCRA

func NewGCRA(client redis.Cmdable, rateLimit int64, period time.Duration, opts ...Option) *GCRALimiter

func (*GCRALimiter) Allow

func (l *GCRALimiter) Allow(ctx context.Context, key string) (*Result, error)

* Allow evaluates the rate limit via Redis Lua script, failing over to local in-memory fallback on error. * * @param ctx - The execution context for cancellation and timeouts. * @param key - The unique client identifier (e.g. API key, IP address). * @return A Result pointer with remaining quota and reset durations, and an error.

type InMemoryFixedWindow

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

func NewInMemoryFixedWindow

func NewInMemoryFixedWindow(limit int64, window time.Duration) *InMemoryFixedWindow

func (*InMemoryFixedWindow) Allow

func (l *InMemoryFixedWindow) Allow(ctx context.Context, key string) (*Result, error)

type InMemoryGCRA

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

func NewInMemoryGCRA

func NewInMemoryGCRA(rateLimit int64, period time.Duration) *InMemoryGCRA

func (*InMemoryGCRA) Allow

func (l *InMemoryGCRA) Allow(ctx context.Context, key string) (*Result, error)

* Allow evaluates the rate limit locally using the GCRA (Generic Cell Rate Algorithm): * * @param ctx - The execution context for cancellation and timeouts. * @param key - The unique client identifier (e.g. API key, IP address). * @return A Result pointer with remaining quota and reset durations, and an error. * * @note Mathematics of GCRA: * 1. Emission Interval (T) = period / rateLimit. (Perfect spacing between requests). * 2. Burst Tolerance (tau) = period. (Allows requests to arrive in bursts up to the period size). * 3. We calculate newTAT = max(oldTAT, now) + T. * 4. If newTAT - now > tau, the rate limit is exceeded. The request is rejected, and the client * must wait for (newTAT - tau - now) before retrying. * 5. If allowed, we update TAT = newTAT.

type InMemorySlidingWindowCounter

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

func NewInMemorySlidingWindowCounter

func NewInMemorySlidingWindowCounter(limit int64, window time.Duration) *InMemorySlidingWindowCounter

func (*InMemorySlidingWindowCounter) Allow

* Allow evaluates the rate limit locally using the Sliding Window Counter algorithm: * * @param ctx - The execution context for cancellation and timeouts. * @param key - The unique client identifier (e.g. API key, IP address). * @return A Result pointer with remaining requests and reset durations, and an error. * * @note Weighted-Average Approximation: * 1. Time is partitioned into fixed window slots (e.g. 1-minute blocks). * 2. We keep track of the request count in the current block (currCount) and the previous block (prevCount). * 3. We calculate the percentage progress through the current block: * weight = (windowMs - elapsedMsInCurrentBlock) / windowMs * 4. We approximate the request count in the sliding window as: * estimatedCount = prevCount * weight + currCount * 5. This achieves ~99% accuracy with O(1) memory footprint.

type InMemorySlidingWindowLog

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

func NewInMemorySlidingWindowLog

func NewInMemorySlidingWindowLog(limit int64, window time.Duration) *InMemorySlidingWindowLog

func (*InMemorySlidingWindowLog) Allow

func (l *InMemorySlidingWindowLog) Allow(ctx context.Context, key string) (*Result, error)

type InMemoryTokenBucket

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

func NewInMemoryTokenBucket

func NewInMemoryTokenBucket(capacity int64, refillRateSec float64) *InMemoryTokenBucket

func (*InMemoryTokenBucket) Allow

func (l *InMemoryTokenBucket) Allow(ctx context.Context, key string) (*Result, error)

* Allow evaluates the rate limit locally using the Token Bucket state logic: * * @param ctx - The execution context for cancellation and timeouts. * @param key - The unique client identifier (e.g. API key, IP address). * @return A Result pointer with remaining tokens and reset durations, and an error. * * @note Mathematics of Token Bucket: * 1. Tokens are replenished continuously over time based on the elapsed duration: * tokens = min(capacity, currentTokens + elapsedSeconds * refillRatePerSecond) * 2. If the current token count is less than the requested cost (default 1), * the request is blocked, and we return the time to wait until enough tokens refill. * 3. Otherwise, we deduct the tokens and return Allowed with the remaining token count.

type Limiter

type Limiter interface {
	Allow(ctx context.Context, key string) (*Result, error)
}

type Logger

type Logger interface {
	Debugf(format string, args ...interface{})
	Infof(format string, args ...interface{})
	Warnf(format string, args ...interface{})
	Errorf(format string, args ...interface{})
}

type NopLogger

type NopLogger struct{}

func (NopLogger) Debugf

func (n NopLogger) Debugf(format string, args ...interface{})

func (NopLogger) Errorf

func (n NopLogger) Errorf(format string, args ...interface{})

func (NopLogger) Infof

func (n NopLogger) Infof(format string, args ...interface{})

func (NopLogger) Warnf

func (n NopLogger) Warnf(format string, args ...interface{})

type Option

type Option func(*Options)

func WithFallbackOnErr

func WithFallbackOnErr(fallbackOnErr bool) Option

func WithKeyPrefix

func WithKeyPrefix(prefix string) Option

func WithLocalFallback

func WithLocalFallback(fallback Limiter) Option

func WithLogger

func WithLogger(logger Logger) Option

type Options

type Options struct {
	KeyPrefix     string
	Fallback      Limiter
	FallbackOnErr bool
	Logger        Logger
}

func DefaultOptions

func DefaultOptions() Options

type RedisClient

type RedisClient struct {
	Client redis.Cmdable
}

func NewRedis

func NewRedis(addr string) *RedisClient

type Result

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

* Result represents the final evaluation status of a rate limit check. * * @field Allowed - True if the request is permitted, false if blocked. * @field Limit - The maximum number of requests allowed in the configured window. * @field Remaining - The remaining quota in the current window. * @field ResetAfter - Duration remaining until the rate limit window resets/refills. * @field RetryAfter - Time duration the client must wait before retrying (only set if Allowed is false).

type SlidingWindowCounterLimiter

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

func NewSlidingWindowCounter

func NewSlidingWindowCounter(client redis.Cmdable, limit int64, window time.Duration, opts ...Option) *SlidingWindowCounterLimiter

func (*SlidingWindowCounterLimiter) Allow

* Allow evaluates the rate limit via Redis, failing over to local in-memory fallback on error. * * @param ctx - The execution context for cancellation and timeouts. * @param key - The unique client identifier (e.g. API key, IP address). * @return A Result pointer with remaining requests and reset durations, and an error.

type SlidingWindowLogLimiter

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

func NewSlidingWindowLog

func NewSlidingWindowLog(client redis.Cmdable, limit int64, window time.Duration, opts ...Option) *SlidingWindowLogLimiter

func (*SlidingWindowLogLimiter) Allow

func (l *SlidingWindowLogLimiter) Allow(ctx context.Context, key string) (*Result, error)

* Allow evaluates the rate limit via Redis Sorted Sets (ZSET): * * @param ctx - The execution context for cancellation and timeouts. * @param key - The unique client identifier (e.g. API key, IP address). * @return A Result pointer with remaining requests and reset durations, and an error. * * @note Unique member generation: * In a ZSET sliding window, the score is the timestamp, and we store every request. * Since Redis sorted set members must be unique, we generate a unique member value * by concatenating the nanosecond timestamp with a random suffix. This guarantees * that concurrent requests from the same user at the exact same millisecond score * are all recorded uniquely.

type TokenBucketLimiter

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

func NewTokenBucket

func NewTokenBucket(client redis.Cmdable, capacity int64, refillRateSec float64, opts ...Option) *TokenBucketLimiter

func (*TokenBucketLimiter) Allow

func (l *TokenBucketLimiter) Allow(ctx context.Context, key string) (*Result, error)

* Allow evaluates the rate limit via Redis hashes, failing over to local in-memory fallback on error. * * @param ctx - The execution context for cancellation and timeouts. * @param key - The unique client identifier (e.g. API key, IP address). * @return A Result pointer with remaining tokens and reset durations, and an error.

Directories

Path Synopsis
cmd
simulator command
examples
basic command
gin_middleware command
middleware
gin

Jump to

Keyboard shortcuts

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