Skip to main content

Overview

The API Gateway implements rate limiting using ASP.NET Core’s built-in rate limiting middleware to protect backend services from excessive requests and potential abuse.

Current Configuration

Rate limiting is currently applied only to the Ordering Service route.

Rate Limiter Implementation

Configured in Program.cs:
using Microsoft.AspNetCore.RateLimiting;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRateLimiter(rateLimiterOptions =>
{
    rateLimiterOptions.AddFixedWindowLimiter("fixed", options =>
    {
        options.Window = TimeSpan.FromSeconds(10);
        options.PermitLimit = 5;
    });
});

var app = builder.Build();

app.UseRateLimiter();
app.MapReverseProxy();

app.Run();

Rate Limiter Policy Details

Policy Name: fixed Algorithm: Fixed Window Configuration:
  • Window Duration: 10 seconds
  • Permit Limit: 5 requests per window
  • Requests Allowed: 5 requests every 10 seconds
  • Rate: 0.5 requests/second average

How Fixed Window Works

Time:     0s          10s         20s         30s
          |-----------|-----------|-----------|--->
Requests: [1,2,3,4,5] [1,2,3,4,5] [1,2,3,4,5]
          ✓ ✓ ✓ ✓ ✓   ✓ ✓ ✓ ✓ ✓   ✓ ✓ ✓ ✓ ✓
          [6,7,8]       [6,7]       [6]
          ✗ ✗ ✗         ✗ ✗         ✗
  • Requests are counted in fixed 10-second windows
  • Counter resets at the start of each window
  • First 5 requests succeed, subsequent requests are rejected until next window
  • Simple and predictable behavior

Applying Rate Limiting to Routes

Rate limiting is enabled per route using the RateLimiterPolicy property:
{
  "ReverseProxy": {
    "Routes": {
      "ordering-route": {
        "ClusterId": "ordering-cluster",
        "RateLimiterPolicy": "fixed",
        "Match": {
          "Path": "/ordering-service/{**catch-all}"
        },
        "Transforms": [ { "PathPattern": "{**catch-all}" } ]
      }
    }
  }
}

Protected Routes

RouteRate LimitedPolicyReason
/catalog-service/*No-Read-heavy operations, high throughput needed
/basket-service/*No-Frequent user interactions expected
/ordering-service/*YesfixedWrite operations, critical business logic

Why Only Ordering Service?

The Ordering Service has rate limiting because:
  1. Write Operations: Creates orders, processes payments, updates inventory
  2. Business Critical: Order creation affects multiple downstream systems
  3. Resource Intensive: Database writes, external service calls, event publishing
  4. Fraud Prevention: Prevents rapid-fire order submission attacks
  5. Resource Protection: Limits load on ordering database and infrastructure

Rate Limit Responses

Success Response

When within rate limit:
HTTP/1.1 200 OK
Content-Type: application/json

{ "orderId": "12345", ... }

Rate Limited Response

When limit exceeded:
HTTP/1.1 429 Too Many Requests
Retry-After: 7

Status Code: 429 Too Many Requests Retry-After Header: Indicates seconds until the next window (remaining time in current window)

Testing Rate Limiting

Using curl

# Send 6 requests rapidly to ordering service
for i in {1..6}; do
  echo "Request $i:"
  curl -i http://localhost:8080/ordering-service/api/v1/orders
  echo ""
done
Expected Output:
  • Requests 1-5: 200 OK
  • Request 6: 429 Too Many Requests

Using PowerShell

1..6 | ForEach-Object {
    Write-Host "Request $_:"
    Invoke-WebRequest -Uri "http://localhost:8080/ordering-service/api/v1/orders" -Method GET
}

Load Testing

# Using Apache Bench
ab -n 100 -c 10 http://localhost:8080/ordering-service/api/v1/orders

# Using hey
hey -n 100 -c 10 http://localhost:8080/ordering-service/api/v1/orders

Rate Limiting Algorithms

ASP.NET Core supports multiple rate limiting algorithms:

1. Fixed Window (Current)

rateLimiterOptions.AddFixedWindowLimiter("fixed", options =>
{
    options.Window = TimeSpan.FromSeconds(10);
    options.PermitLimit = 5;
});
Pros: Simple, predictable Cons: Burst at window boundaries

2. Sliding Window

rateLimiterOptions.AddSlidingWindowLimiter("sliding", options =>
{
    options.Window = TimeSpan.FromSeconds(10);
    options.PermitLimit = 5;
    options.SegmentsPerWindow = 2; // 5-second segments
});
Pros: Smoother rate limiting Cons: More complex, higher memory usage

3. Token Bucket

rateLimiterOptions.AddTokenBucketLimiter("token", options =>
{
    options.TokenLimit = 5;
    options.TokensPerPeriod = 5;
    options.ReplenishmentPeriod = TimeSpan.FromSeconds(10);
});
Pros: Allows controlled bursts Cons: More complex configuration

4. Concurrency

rateLimiterOptions.AddConcurrencyLimiter("concurrent", options =>
{
    options.PermitLimit = 10;
    options.QueueLimit = 5;
});
Pros: Limits simultaneous requests Cons: Not time-based

Advanced Configuration

Per-User Rate Limiting

builder.Services.AddRateLimiter(rateLimiterOptions =>
{
    rateLimiterOptions.AddFixedWindowLimiter("per-user", options =>
    {
        options.Window = TimeSpan.FromMinutes(1);
        options.PermitLimit = 10;
    });
    
    // Partition by user ID
    rateLimiterOptions.AddPolicy("user-specific", context =>
    {
        var userId = context.User.Identity?.Name ?? "anonymous";
        return RateLimitPartition.GetFixedWindowLimiter(userId, _ => new FixedWindowRateLimiterOptions
        {
            Window = TimeSpan.FromMinutes(1),
            PermitLimit = 10
        });
    });
});

Per-IP Rate Limiting

rateLimiterOptions.AddPolicy("per-ip", context =>
{
    var ipAddress = context.Connection.RemoteIpAddress?.ToString() ?? "unknown";
    return RateLimitPartition.GetFixedWindowLimiter(ipAddress, _ => new FixedWindowRateLimiterOptions
    {
        Window = TimeSpan.FromMinutes(1),
        PermitLimit = 20
    });
});

Custom Rejection Response

rateLimiterOptions.OnRejected = async (context, token) =>
{
    context.HttpContext.Response.StatusCode = 429;
    await context.HttpContext.Response.WriteAsJsonAsync(new
    {
        error = "Too many requests",
        message = "Please try again later",
        retryAfter = context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter) 
            ? retryAfter.TotalSeconds 
            : null
    }, cancellationToken: token);
};

Monitoring and Metrics

Key Metrics to Track

  1. Total Requests: Number of requests per route
  2. Rate Limited Requests: Count of 429 responses
  3. Rate Limit Percentage: (429s / Total Requests) * 100
  4. Window Utilization: Average permits used per window

Logging Rate Limit Events

rateLimiterOptions.OnRejected = async (context, token) =>
{
    var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<Program>>();
    logger.LogWarning(
        "Rate limit exceeded for {Path} from {IP}",
        context.HttpContext.Request.Path,
        context.HttpContext.Connection.RemoteIpAddress
    );
    
    context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
};

Recommendations

Current Setup

The current configuration (5 requests per 10 seconds) is very restrictive and likely intended for development/testing.

Production Recommendations

Ordering Service

// More realistic production limits
options.Window = TimeSpan.FromMinutes(1);
options.PermitLimit = 30; // 30 requests per minute

Catalog Service (if needed)

// Higher limits for read operations
options.Window = TimeSpan.FromMinutes(1);
options.PermitLimit = 100; // 100 requests per minute

Basket Service (if needed)

// Moderate limits for user interactions
options.Window = TimeSpan.FromMinutes(1);
options.PermitLimit = 50; // 50 requests per minute

Best Practices

  1. Monitor First: Track request patterns before applying limits
  2. Start Permissive: Begin with higher limits and tighten as needed
  3. User-Specific: Implement per-user limits for better fairness
  4. Gradual Degradation: Use queuing instead of immediate rejection
  5. Clear Communication: Provide meaningful error messages
  6. Document Limits: Communicate rate limits to API consumers

Adding Rate Limiting to Other Routes

To add rate limiting to catalog or basket services:
  1. Define a new policy in Program.cs:
rateLimiterOptions.AddFixedWindowLimiter("catalog-limit", options =>
{
    options.Window = TimeSpan.FromMinutes(1);
    options.PermitLimit = 100;
});
  1. Apply to route in appsettings.json:
"catalog-route": {
  "ClusterId": "catalog-cluster",
  "RateLimiterPolicy": "catalog-limit",
  "Match": {
    "Path": "/catalog-service/{**catch-all}"
  },
  "Transforms": [ { "PathPattern": "{**catch-all}" } ]
}

Build docs developers (and LLMs) love