Backend guides
Rate limiting
Apply and tune rate-limit policies on endpoints, and the defaults that ship with the template.
On this page
How it works
Rate limiting is a backend concern. A request that exhausts its partition is rejected with HTTP
429 Too Many Requests before the handler runs. The frontend treats that as just another error: the
API client surfaces it and the calling code can show a toast. See the API client
for how 429s reach the UI.
Policies are named and defined once, in api/src/Slicekit.Api/Configuration/RateLimiting.cs. An
endpoint opts into one with .RequireRateLimiting(RateLimitPolicies.<Name>). Each policy declares a
partition key (who shares a quota) and a limiter (how many requests, over what window).
Shipped policies
Six policies cover the template’s needs. Most new endpoints reuse one of these rather than adding a seventh.
| Policy | Partition key | Limit |
|---|---|---|
RateLimitPolicies.Default |
sub claim, fallback to remote IP |
100 / minute |
RateLimitPolicies.Anonymous |
Remote IP | 20 / minute |
RateLimitPolicies.Auth |
Remote IP | 10 / minute |
RateLimitPolicies.StepUp |
sub claim, fallback to remote IP |
10 / minute |
RateLimitPolicies.CreateApiKey |
sub claim, fallback to remote IP |
10 / minute |
RateLimitPolicies.ExportData |
sub claim, fallback to remote IP |
5 / 24 hours |
Anonymous and Auth partition by IP because the caller is not yet authenticated when those
endpoints are hit, so there is no sub claim to key on. Everything else partitions by user (sub),
which means several users behind one NAT do not share a quota.
StepUp covers the operations that re-check the password behind a session you already have: deleting
the account, changing the email, disabling 2FA, changing the password, regenerating recovery codes.
The counter is shared across replicas
The limits above are the deployment’s, not each replica’s. Counting in process is the obvious way to build a limiter and the wrong one behind a load balancer: three replicas each enforcing “10 / minute” add up to 30, on the authentication endpoints as much as anywhere else. Slicekit counts in Redis when you have configured one, with the increment and the window read in a single script so concurrent requests cannot lose a count to a race. With no Redis, or with Redis unreachable, it falls back to counting in process, which is the weaker behaviour but never the absent one.
Applying a policy to an endpoint
Add .RequireRateLimiting(...) to the route, alongside the other route policies (authorization,
validation, CSRF). The endpoint stays thin: it declares the limit, it does not enforce it by hand.
See adding a vertical slice for the full endpoint shape.
public static void Map(IEndpointRouteBuilder routes) =>
routes.Auth().MapPost("/register", HandleAsync)
.WithName("Auth_Register")
.AllowAnonymous()
.RequireRateLimiting(RateLimitPolicies.Auth)
.ProducesProblem(429);
.ProducesProblem(429) is mandatory whenever an endpoint declares a rate limit. The 429 is part of
the public contract, so it belongs in the OpenAPI document the same way any other failure response
does.
Adding a new policy
Reach for a new policy only when none of the six fit. A new policy is a constant and a registration.
-
Add a
const stringonRateLimitPolicies:public const string PasswordReset = "password-reset"; -
Register it inside
ConfigureRateLimiting:Window(opts, RateLimitPolicies.PasswordReset, GetRemoteIp, "rl:password-reset", permitLimit: 3, window: TimeSpan.FromHours(1)); -
Reference it from the endpoint with
.RequireRateLimiting(RateLimitPolicies.PasswordReset).
The shared limiter rejects immediately once the trailing-window total is exhausted. It does not queue HTTP requests.
Tuning the limits
Two knobs do most of the work: the partition key (who shares the quota) and the window and segment sizes (how the count is spread over time).
Window and segments.
All shipped policies use the shared sliding-window counter. More segments return capacity more
gradually and require more bucket reads. ExportData uses 24 one-hour segments, so its allowance
returns gradually across the 24-hour window.
Partition key.
- User-scoped operation: key on the
subclaim, falling back to IP with?? GetRemoteIp(context). The fallback covers misconfigured auth and any endpoint mistakenly tagged with a user policy while anonymous. - Unauthenticated endpoint: key on IP only, since
subis absent. - Cross-tenant admin action: prefer
sub. Keying on IP would let one admin’s quota be eaten by another admin behind the same NAT.
To change an existing limit, edit PermitLimit and Window on the policy in question. Nothing else
references the numbers.
Verify
dotnet buildpasses.- Start the API, hit a rate-limited endpoint past its limit, and confirm a
429 Too Many Requestswith a problem-details body. - Check the OpenAPI document at
/scalar: every endpoint that declares.ProducesProblem(429)shows the 429 response.
Checklist
- Endpoint declares
.RequireRateLimiting(RateLimitPolicies.<Name>). - Endpoint declares
.ProducesProblem(429)to match. - The chosen policy’s partition key fits the endpoint (user-scoped uses
sub, public uses IP). - A new policy was added only because none of the six shipped policies fit.
- New policies use the shared
Windowhelper. -
dotnet buildpasses and the 429 shows up in/scalar.