Isaac.

API Rate Limiting Implementation

Implement rate limiting to protect APIs from abuse.

By EMEPublished: February 20, 2025
rate limitingapi protectionthrottlingabuse prevention

A Simple Analogy

Rate limiting is like a bouncer at a club. It controls how many people (requests) enter per time period to maintain capacity.


Why Rate Limiting?

  • Abuse prevention: Stop malicious requests
  • Resource protection: Prevent overload
  • Fair usage: Ensure access for all users
  • Cost control: Limit API costs
  • SLA compliance: Maintain service quality

Token Bucket Algorithm

public class TokenBucket
{
    private readonly int _capacity;
    private readonly double _refillRate;
    private double _tokens;
    private DateTime _lastRefillTime;
    private readonly object _lock = new();
    
    public TokenBucket(int capacity, double refillRate)
    {
        _capacity = capacity;
        _refillRate = refillRate;
        _tokens = capacity;
        _lastRefillTime = DateTime.UtcNow;
    }
    
    public bool TryConsumeToken(int tokensRequired = 1)
    {
        lock (_lock)
        {
            Refill();
            
            if (_tokens >= tokensRequired)
            {
                _tokens -= tokensRequired;
                return true;
            }
            
            return false;
        }
    }
    
    private void Refill()
    {
        var now = DateTime.UtcNow;
        var elapsed = (now - _lastRefillTime).TotalSeconds;
        _tokens = Math.Min(_capacity, _tokens + (elapsed * _refillRate));
        _lastRefillTime = now;
    }
}

Per-User Rate Limiting

public class RateLimitMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ConcurrentDictionary<string, TokenBucket> _buckets;
    
    public RateLimitMiddleware(RequestDelegate next)
    {
        _next = next;
        _buckets = new ConcurrentDictionary<string, TokenBucket>();
    }
    
    public async Task InvokeAsync(HttpContext context)
    {
        var userId = context.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? 
                     context.Connection.RemoteIpAddress?.ToString();
        
        if (userId == null)
        {
            await _next(context);
            return;
        }
        
        var bucket = _buckets.GetOrAdd(userId, _ => new TokenBucket(
            capacity: 100,
            refillRate: 10  // 10 requests per second
        ));
        
        if (!bucket.TryConsumeToken())
        {
            context.Response.StatusCode = StatusCodes.Status429TooManyRequests;
            context.Response.Headers.Add("Retry-After", "60");
            await context.Response.WriteAsync("Rate limit exceeded");
            return;
        }
        
        await _next(context);
    }
}

Header-Based Rate Limits

public class RateLimitResponseMiddleware
{
    private readonly RequestDelegate _next;
    
    public async Task InvokeAsync(HttpContext context, IRateLimitService rateLimitService)
    {
        var limit = await rateLimitService.GetLimitAsync(context);
        var remaining = limit.Remaining;
        var resetTime = limit.ResetTime;
        
        context.Response.Headers.Add("X-RateLimit-Limit", limit.Limit.ToString());
        context.Response.Headers.Add("X-RateLimit-Remaining", remaining.ToString());
        context.Response.Headers.Add("X-RateLimit-Reset", 
            new DateTimeOffset(resetTime).ToUnixTimeSeconds().ToString());
        
        await _next(context);
    }
}

Redis-Based Distributed Rate Limiting

public class RedisRateLimitService
{
    private readonly IConnectionMultiplexer _redis;
    
    public async Task<bool> IsAllowedAsync(string userId, int limit, int windowSeconds)
    {
        var db = _redis.GetDatabase();
        var key = $"rate-limit:{userId}";
        
        var current = await db.StringIncrementAsync(key);
        
        if (current == 1)
        {
            await db.KeyExpireAsync(key, TimeSpan.FromSeconds(windowSeconds));
        }
        
        return current <= limit;
    }
}

// Usage
var isAllowed = await _rateLimitService.IsAllowedAsync(
    userId: "user-123",
    limit: 100,
    windowSeconds: 60
);

if (!isAllowed)
{
    return StatusCode(429);
}

Endpoint-Specific Limits

[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
    [HttpGet]
    [RateLimit(requests: 100, windowSeconds: 60)]
    public async Task<IActionResult> GetOrders()
    {
        return Ok(await _service.GetOrdersAsync());
    }
    
    [HttpPost]
    [RateLimit(requests: 10, windowSeconds: 60)]  // Stricter limit for writes
    public async Task<IActionResult> CreateOrder(CreateOrderRequest request)
    {
        var order = await _service.CreateAsync(request);
        return CreatedAtAction(nameof(GetOrders), new { id = order.Id }, order);
    }
}

Best Practices

  1. User-based limits: Limit by user ID
  2. IP-based limits: Fallback for anonymous users
  3. Tiered limits: Different limits for different plans
  4. Clear headers: Inform clients of limits
  5. Graceful degradation: Queue requests instead of rejecting

Related Concepts

  • Throttling strategies
  • Quota management
  • Load balancing
  • API gateway patterns

Summary

Rate limiting protects APIs from abuse and ensures fair resource allocation. Use token bucket algorithm, per-user tracking, and clear response headers.