Skip to content
Slicekit

Backend guides

Rate limiting

Apply and tune rate-limit policies on endpoints, and the defaults that ship with the template.

View .md
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.

  1. Add a const string on RateLimitPolicies:

    public const string PasswordReset = "password-reset";
  2. Register it inside ConfigureRateLimiting:

    Window(opts, RateLimitPolicies.PasswordReset, GetRemoteIp, "rl:password-reset",
        permitLimit: 3, window: TimeSpan.FromHours(1));
  3. 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 sub claim, 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 sub is 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 build passes.
  • Start the API, hit a rate-limited endpoint past its limit, and confirm a 429 Too Many Requests with 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 Window helper.
  • dotnet build passes and the 429 shows up in /scalar.

Live demo

Explore the running app

No sign-up. Pick an account to open the demo with its credentials prefilled, or open it and sign in yourself.

Open without signing in

Credentials are prefilled. Shared password Slicekit-Demo-2026!.