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