How to implement like bucket rate limit?

Asked 20 days ago
Updated 16 days ago
Viewed 98 times

0

1 Answer


0

A token bucket rate limiter is the most practical and production-friendly way to protect an API. Below is a clean, real-world implementation guide, starting from concept → ASP.NET API code → distributed (Redis) version → best practices.

I’ll focus on ASP.NET (MVC / Web API) since that’s what you usually work with.

1. Token Bucket – How It Works (Quick)

  • Bucket has capacity (max tokens)
  • Tokens refill at a fixed rate
  • Each request consumes 1 token
  • If no token → 429 Too Many Requests

Example:

Capacity = 100 tokens
Refill = 10 tokens / second

Allows:

  • Short bursts
  • Controlled average rate

2. In-Memory Token Bucket (Single Server)

Step 1: Token Bucket Model

public class TokenBucket
{
    public int Capacity { get; }
    public double Tokens { get; private set; }
    public double RefillRate { get; } // tokens per second
    public DateTime LastRefill { get; private set; }

    private readonly object _lock = new object();

    public TokenBucket(int capacity, double refillRate)
    {
        Capacity = capacity;
        RefillRate = refillRate;
        Tokens = capacity;
        LastRefill = DateTime.UtcNow;
    }

    public bool TryConsume(int tokens = 1)
    {
        lock (_lock)
        {
            Refill();

            if (Tokens >= tokens)
            {
                Tokens -= tokens;
                return true;
            }

            return false;
        }
    }

    private void Refill()
    {
        var now = DateTime.UtcNow;
        var seconds = (now - LastRefill).TotalSeconds;

        if (seconds <= 0) return;

        Tokens = Math.Min(Capacity, Tokens + seconds * RefillRate);
        LastRefill = now;
    }
}

Step 2: Rate Limit Store

public static class RateLimitStore
{
    private static readonly ConcurrentDictionary<string, TokenBucket> Buckets
        = new ConcurrentDictionary<string, TokenBucket>();

    public static TokenBucket GetBucket(string key)
    {
        return Buckets.GetOrAdd(key,
            _ => new TokenBucket(capacity: 100, refillRate: 10));
    }
}

Step 3: API Filter (ASP.NET MVC / Web API)

public class TokenBucketRateLimitAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext context)
    {
        var request = context.HttpContext.Request;
        var key = request.UserHostAddress; // IP based

        var bucket = RateLimitStore.GetBucket(key);

        if (!bucket.TryConsume())
        {
            context.Result = new HttpStatusCodeResult(429, "Rate limit exceeded");
            return;
        }

        base.OnActionExecuting(context);
    }
}

Step 4: Use It

[TokenBucketRateLimit]
public ActionResult GetUsers()
{
    return Json(data, JsonRequestBehavior.AllowGet);
}

3. Client Identification (Important)

You can rate-limit by:

Type Key
IP UserHostAddress
API Key X-Api-Key
User ID JWT claim
Endpoint METHOD:/api/users

Example:

var key = $"{userId}:{request.HttpMethod}:{request.Path}";

4. Distributed Token Bucket (Redis – Production)

For multiple servers, memory won’t work.

Redis Token Bucket (Atomic)

Redis Lua Script

local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local refill_rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])

local bucket = redis.call("HMGET", key, "tokens", "timestamp")

local tokens = tonumber(bucket[1]) or capacity
local last = tonumber(bucket[2]) or now

local delta = math.max(0, now - last)
tokens = math.min(capacity, tokens + delta * refill_rate)

if tokens < 1 then
    redis.call("HMSET", key, "tokens", tokens, "timestamp", now)
    return 0
end

tokens = tokens - 1
redis.call("HMSET", key, "tokens", tokens, "timestamp", now)
redis.call("EXPIRE", key, 3600)

return 1

C# Redis Call (StackExchange.Redis)

var result = (int)redis.ScriptEvaluate(
    luaScript,
    new RedisKey[] { key },
    new RedisValue[]
    {
        capacity,
        refillRate,
        DateTimeOffset.UtcNow.ToUnixTimeSeconds()
    });

if (result == 0)
{
    return StatusCode(429);
}

5. Response Headers (Recommended)

Always tell clients their limit:

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 42
X-RateLimit-Reset: 1700000123

6. ASP.NET 7+ Built-in Alternative (FYI)

builder.Services.AddRateLimiter(options =>
{
    options.AddTokenBucketLimiter("api", opt =>
    {
        opt.TokenLimit = 100;
        opt.ReplenishmentPeriod = TimeSpan.FromSeconds(1);
        opt.TokensPerPeriod = 10;
    });
});

But since you’re not on ASP.NET Core, the custom approach above is correct.

7. Which Algorithm Should You Use?

Scenario Best
Bursty traffic Token Bucket
Strict per minute Fixed Window
Smooth traffic Leaky Bucket
Distributed Token Bucket + Redis

8. Common Mistakes

  • Using Thread.Sleep
  • No locking / atomicity
  • No TTL cleanup
  • Per-request DB hit
  • Rate limiting only by IP

9. Recommended Defaults

Use Case Capacity Refill
Public API 100 10/sec
Auth API 20 2/sec
Internal API 1000 100/sec

10. Want a Production-Ready Version?

I can:

  • Convert this into HTTP module / middleware
  • Add per-endpoint config
  • Add Redis + fallback memory
  • Add sliding window analytics
  • Add admin override / whitelist

 

answered 16 days ago by ICSM

Your Answer