Public APIs need more than authentication. A valid user, an impatient frontend, a retrying background worker, or a badly configured client can still send enough traffic to slow down your application. Rate limiting gives your ASP.NET Core API a simple safety layer before the request reaches expensive business logic, database calls, or third-party integrations.
In this guide, we will add built-in ASP.NET Core rate limiting middleware, create separate policies for normal API traffic and sensitive endpoints, return a clean 429 Too Many Requests response, and discuss production checks you should do before shipping.
Why rate limiting matters in ASP.NET Core APIs
Rate limiting controls how many requests a client can make in a specific period of time. For backend developers, it helps with three common problems:
- Protecting expensive endpoints such as report generation, AI calls, file uploads, and search.
- Reducing abuse on public APIs, login endpoints, and anonymous forms.
- Keeping the system stable when clients retry too aggressively during failures.
Rate limiting is not a replacement for authentication, authorization, caching, request validation, WAF rules, or infrastructure-level protection. Think of it as one layer in your API reliability design.
Install and configure the middleware
ASP.NET Core includes rate limiting support through Microsoft.AspNetCore.RateLimiting and the limiter types from System.Threading.RateLimiting. A simple production-friendly starting point is a fixed window policy for general API traffic.
using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRateLimiter(options =>
{
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
options.AddFixedWindowLimiter("api", limiterOptions =>
{
limiterOptions.PermitLimit = 100;
limiterOptions.Window = TimeSpan.FromMinutes(1);
limiterOptions.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
limiterOptions.QueueLimit = 0;
});
});
var app = builder.Build();
app.UseRateLimiter();
app.MapGet("/api/products", () => Results.Ok(new[] { "Tea", "Chai", "Coffee" }))
.RequireRateLimiting("api");
app.Run();
This policy allows 100 requests per minute for endpoints that opt into the api policy. The QueueLimit = 0 setting rejects extra requests immediately instead of making clients wait in a server-side queue.
Add a stricter policy for sensitive endpoints
Not every endpoint should use the same limit. A login endpoint, OTP endpoint, contact form, or AI generation endpoint needs stricter protection than a simple read-only list endpoint.
builder.Services.AddRateLimiter(options =>
{
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
options.AddFixedWindowLimiter("api", opt =>
{
opt.PermitLimit = 100;
opt.Window = TimeSpan.FromMinutes(1);
opt.QueueLimit = 0;
});
options.AddFixedWindowLimiter("sensitive", opt =>
{
opt.PermitLimit = 5;
opt.Window = TimeSpan.FromMinutes(1);
opt.QueueLimit = 0;
});
});
app.MapPost("/api/auth/login", LoginAsync)
.RequireRateLimiting("sensitive");
app.MapPost("/api/contact", SendContactMessageAsync)
.RequireRateLimiting("sensitive");
This keeps normal API traffic usable while reducing brute-force and spam pressure on sensitive routes.
Return a helpful 429 response
A plain 429 status code is technically correct, but API consumers benefit from a consistent response body. You can customize the rejection behavior with OnRejected.
builder.Services.AddRateLimiter(options =>
{
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
options.OnRejected = async (context, cancellationToken) =>
{
context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
context.HttpContext.Response.ContentType = "application/json";
await context.HttpContext.Response.WriteAsJsonAsync(new
{
error = "rate_limit_exceeded",
message = "Too many requests. Please wait and try again."
}, cancellationToken);
};
});
If your clients are mobile apps, SPAs, or partner integrations, this small detail improves debugging and makes retry behavior easier to implement.
Choose the right limiter
ASP.NET Core supports multiple limiter styles. Pick one based on the endpoint cost and traffic pattern:
- Fixed window: Simple and easy to reason about. Good default for many APIs.
- Sliding window: Smoother than fixed window because the time period is split into smaller segments.
- Token bucket: Useful when you want to allow short bursts but still control long-term request rate.
- Concurrency limiter: Limits active requests at the same time. Helpful for expensive operations like exports, AI calls, and large uploads.
Production checklist
Before enabling rate limiting in production, review these points:
- Do load testing. Validate limits with real traffic patterns before rollout.
- Watch 429 metrics. A sudden increase may mean abuse, a frontend retry bug, or limits that are too strict.
- Separate endpoint policies. Login, contact, AI, export, and upload endpoints deserve different limits.
- Be careful with partition keys. If you rate limit by IP, understand proxy headers, NAT, spoofing concerns, and hosting infrastructure.
- Document client behavior. API consumers should know when to retry and how to handle 429 responses.
Common mistake: one policy for everything
The easiest setup is one global limit across the whole API, but it is rarely the best final design. A public product listing endpoint, a login endpoint, and an AI-powered summary endpoint do not have the same cost or risk. Start simple, then split policies as your API grows.
Final thoughts
Rate limiting is one of those backend features that feels small until the day you need it. Adding it early gives your ASP.NET Core Web API a stronger production posture, protects your database and downstream services, and creates a better experience for well-behaved clients.
If you are building APIs with ASP.NET Core, Docker, Kubernetes, or AWS, make rate limiting part of your normal production checklist.