Skip to content

Conversation

Umang01-hash
Copy link
Member

Add Rate Limiting Support for HTTP Services

This PR adds rate limiting capabilities to # Add Rate Limiting Support for HTTP Services

This PR adds rate limiting capabilities to GoFr's HTTP service clients using the token bucket algorithm. It provides both local (in-memory) and distributed (Redis-based) implementations to suit different deployment scenarios.

Features

  • Two Implementation Strategies:

    • Local (In-Memory): For single-instance deployments
    • Distributed (Redis): For multi-instance production deployments using atomic Lua scripts
  • Core Features:

    • Configurable requests per second (RPS) and burst limits
    • Sub-1 RPS support (e.g., 0.5 requests/second)
    • Custom service key extraction support

Usage Example

// For distributed rate limiting (multi-instance)
a.AddHTTPService("payment-api", "http://localhost:9005",
    &service.RateLimiterConfig{
        RequestsPerSecond: 1,    // 1 request per second
        Burst:            3,     // Allow bursts up to 3 requests
        RedisClient:      rc,    // Redis client for distributed limiting
    },
)

// For local rate limiting (single instance)
a.AddHTTPService("payment-api", "http://localhost:9005",
    &service.RateLimiterConfig{
        RequestsPerSecond: 1,    // 1 request per second
        Burst:            3,     // Allow bursts up to 3 requests
    },
) 

Usage Screenshots:

  • Logs
Screenshot 2025-09-12 at 11 17 31 AM
  • Metrics
Screenshot 2025-09-12 at 11 18 24 AM

Checklist:

  • I have formatted my code using goimport and golangci-lint.
  • All new code is covered by unit tests.
  • This PR does not decrease the overall code coverage.
  • I have reviewed the code comments and documentation for clarity.

Thank you for your contribution!

coolwednesday
coolwednesday previously approved these changes Sep 18, 2025
@akshat-kumar-singhal
Copy link
Contributor

@Umang01-hash We should consider having support for generic time window, i.e. not limit to per second. Most systems have per minute/hour limits.

@Umang01-hash
Copy link
Member Author

@akshat-kumar-singhal sure fixed the same

Users can use it like:

// Per-second rate limiting
app.AddHTTPService("fast-api", "https://fast.api.com",
    &service.RateLimiterConfig{
        Requests: 10,           // 10 requests per second
        Window:   time.Second,  // Explicit time window
        Burst:    15,
    },
)

// Per-hour rate limiting
app.AddHTTPService("batch-api", "https://batch.api.com",
    &service.RateLimiterConfig{
        Requests: 1000,        // 1000 requests per hour
        Window:   time.Hour,   // Hourly window
        Burst:    1200,
    },
)

Copy link
Member

@coolwednesday coolwednesday left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Validate Window ≥1 ms
    Prevent micro/nanosecond windows that spike CPU and precision issues:
    if config.Window<time.Millisecond { return fmt.Errorf("min window 1ms, got %v",config.Window) }

  2. RPS Calculation
    Avoid repeated division: compute once and reuse cachedRPS.

  3. Preload Lua Script
    Load script SHA on init and use EVALSHA to skip re-compiling on every Redis call.


type Logger interface {
Log(args ...any)
Debug(args ...any)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Altering an exported interface is a breaking change

httpSvc := h.(*httpService)

rl := &distributedRateLimiter{
config: config,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this config should be validated


var (
errInvalidRequestRate = errors.New("requestsPerSecond must be greater than 0")
errInvalidBurstSize = errors.New("burst must be greater than 0")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional validation: Burst size should be greater than requests per unit time

type RateLimiterConfig struct {
Requests float64 // Number of requests allowed
Window time.Duration // Time window (e.g., time.Minute, time.Hour)
Burst int // Maximum burst capacity (must be > 0)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Burst can be optional - set equal to Requests in case not configured/zero.

)

var (
errInvalidRequestRate = errors.New("requestsPerSecond must be greater than 0")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error message should be updated as we are supporting other time windows as well

// distributedRateLimiter with metrics support.
type distributedRateLimiter struct {
config RateLimiterConfig
redisClient *gofrRedis.Redis
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be a "cache interface" or "rate limiter bucket" instance instead of specifically Redis? That way the implementation difference between Redis/Local would just be at the storage level and not in the core functionality

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants