In production environments, APIs face constant threats from malicious actors, web scrapers, and accidental traffic floods that can overwhelm database connections and exhaust server resources. Unprotected endpoints are vulnerable to denial-of-service (DoS) attacks, brute-force login attempts, and redundant database round-trips.
Securing your infrastructure requires a two-pronged defense strategy: Rate Limiting to control traffic velocity and Distributed Caching to absorb heavy read redundancy.
This article walks through a complete, production-grade implementation of built-in rate limiting and Redis distributed caching in an ASP.NET Core Web API.
1. Architectural Strategy: Rate Limiting vs. Caching
Rate Limiting (Throttling): Restricts the number of requests a client can make within a specified time window. It protects your application backend from volumetric abuse and ensures fair usage across clients.
Distributed Caching (Redis): Offloads repetitive read requests by storing frequently accessed data in a fast, in-memory distributed store, eliminating unnecessary database queries.
2. Step 1: Implementing Built-In Rate Limiting (.NET 7+)
ASP.NET Core includes a robust, native rate-limiting middleware that supports four primary algorithms:
Fixed Window: Limits requests to a fixed time block (e.g., 100 requests per minute).
Sliding Window: Divides the window into segments, smoothing out traffic spikes at window boundaries.
Token Bucket: Allows short bursts of traffic while enforcing a strict long-term average refill rate.
Concurrency: Limits the total number of simultaneous requests executing concurrently.
Configuring Rate Limit Policies in Program.cs
C#
// WebApi/Program.cs
using System.Threading.RateLimiting;
using Microsoft.AspNetCore.RateLimiting;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// Configure Rate Limiting Policies
builder.Services.AddRateLimiter(options =>
{
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
// Fixed window policy for standard public endpoints
options.AddPolicy("FixedWindowPolicy", httpContext =>
RateLimitPartition.GetFixedWindowLimiter(
partitionKey: httpContext.User.Identity?.Name ?? httpContext.Connection.RemoteIpAddress?.ToString() ?? "anonymous",
factory: _ => new FixedWindowRateLimiterOptions
{
PermitLimit = 10,
Window = TimeSpan.FromSeconds(10),
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 2
}));
// Strict concurrency policy for sensitive endpoints (e.g., Checkout / Payment)
options.AddConcurrencyLimiter("ConcurrencyPolicy", opt =>
{
opt.PermitLimit = 5;
opt.QueueLimit = 2;
});
});
var app = builder.Build();
// ... configure middleware pipeline
app.UseHttpsRedirection();
// Must be placed after routing and before authorization
app.UseRateLimiter();
app.UseAuthorization();
app.MapControllers();
app.Run();
3. Step 2: Applying Rate Limits to Controllers
Use the [EnableRateLimiting] attribute to secure specific controllers or action methods:
C#
// WebApi/Controllers/ProductsController.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;
namespace WebApi.Controllers;
[ApiController]
[Route("api/[controller]")]
[EnableRateLimiting("FixedWindowPolicy")] // Applies fixed window limit to entire controller
public class ProductsController : ControllerBase
{
[HttpGet]
public IActionResult GetAll()
{
return Ok(new[] { "Product 1", "Product 2" });
}
[HttpPost("checkout")]
[EnableRateLimiting("ConcurrencyPolicy")] // Overrides with strict concurrency limit
public IActionResult Checkout()
{
return Ok(new { message = "Checkout processed successfully." });
}
}
4. Step 3: Implementing Distributed Caching with Redis
To prevent database saturation during high-read traffic spikes, integrate Redis using ASP.NET Core’s IDistributedCache interface and the cache-aside pattern.
First, install the StackExchange Redis caching package:
Shell
dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis
Register Redis in Program.cs:
C#
// WebApi/Program.cs snippet
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = builder.Configuration.GetConnectionString("RedisConnection");
options.InstanceName = "ApiCache_";
});
Implementing the Cache-Aside Pattern in a Service
C#
// Application/Services/CachedProductService.cs
using System.Text.Json;
using Domain.Entities;
using Microsoft.Extensions.Caching.Distributed;
namespace Application.Services;
public class CachedProductService
{
private readonly IProductRepository _productRepository;
private readonly IDistributedCache _cache;
public CachedProductService(IProductRepository productRepository, IDistributedCache cache)
{
_productRepository = productRepository;
_cache = cache;
}
public async Task<Product?> GetByIdAsync(int id, CancellationToken cancellationToken)
{
string cacheKey = $"product-{id}";
// 1. Check cache first
var cachedData = await _cache.GetStringAsync(cacheKey, cancellationToken);
if (!string.IsNullOrEmpty(cachedData))
{
return JsonSerializer.Deserialize<Product>(cachedData);
}
// 2. Fall back to database if cache miss occurs
var product = await _productRepository.GetByIdAsync(id, cancellationToken);
if (product == null) return null;
// 3. Store in Redis cache with an expiration time
var cacheOptions = new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5)
};
await _cache.SetStringAsync(
cacheKey,
JsonSerializer.Serialize(product),
cacheOptions,
cancellationToken);
return product;
}
}
Conclusion
By combining built-in rate-limiting middleware with distributed Redis caching, your ASP.NET Core Web API defends itself against brute-force abuse and traffic spikes while maintaining sub-millisecond response times for read-heavy operations.

Join the conversation! Your thoughts help the community grow.