Skip to content
Written with Claude

Rate Limiter

Rate limiting configuration to control the number of requests from clients. Apply policies to endpoints using the rate_limiter_policy annotation.

Overview

json
json
{
  "RateLimiterOptions": {
    "Enabled": false,
    "StatusCode": 429,
    "StatusMessage": "Too many requests. Please try again later.",
    "DefaultPolicy": null,
    "Policies": {}
  }
}

Breaking change in 3.13.0

RateLimiterOptions:Policies was previously an array of objects with explicit "Name" properties. It is now an object keyed by policy name, matching ValidationOptions:Rules and CacheOptions:Profiles. Migrate by moving each policy's Name value to be the JSON key and dropping the Name field. If you upgrade with the old array form still in your config, startup will fail with a clear InvalidOperationException telling you to migrate.

Settings Reference

SettingTypeDefaultDescription
EnabledboolfalseEnable rate limiting.
StatusCodeint429HTTP status code returned when rate limit is exceeded.
StatusMessagestring"Too many requests. Please try again later."Response message when rate limit is exceeded.
DefaultPolicystringnullName of the default policy to apply to all endpoints.
Policiesobject{}Named rate limiting policies, keyed by policy name. Assign a policy to an endpoint using the rate_limiter_policy annotation.

Policy Types

Four policy types are available:

  • FixedWindow - Fixed time window rate limiting
  • SlidingWindow - Sliding time window rate limiting
  • TokenBucket - Token bucket algorithm
  • Concurrency - Concurrent request limiting

Fixed Window Policy

Limits requests within fixed time intervals.

json
json
{
  "Policies": {
    "fixed": {
      "Type": "FixedWindow",
      "Enabled": true,
      "PermitLimit": 100,
      "WindowSeconds": 60,
      "QueueLimit": 10,
      "AutoReplenishment": true
    }
  }
}

The JSON key ("fixed") is the policy name used with the rate_limiter_policy annotation.

SettingTypeDefaultDescription
Typestring-Must be "FixedWindow".
EnabledboolfalseEnable this policy.
PermitLimitint100Maximum requests allowed per window.
WindowSecondsint60Window duration in seconds.
QueueLimitint10Maximum queued requests when limit is reached.
AutoReplenishmentbooltrueAutomatically replenish permits.
StatusCodeintglobalOptional. HTTP status code returned when this policy rejects a request, overriding RateLimiterOptions:StatusCode. Omit to inherit the global value. See Per-Policy Status Code and Message.
StatusMessagestringglobalOptional. Response message when this policy rejects a request, overriding RateLimiterOptions:StatusMessage. Omit to inherit the global value.
PartitionobjectnullOptional Partition block for per-user / per-IP / per-header rate limiting.

See Fixed Window Limiter documentation.

Sliding Window Policy

Limits requests using a sliding time window with segments.

json
json
{
  "Policies": {
    "sliding": {
      "Type": "SlidingWindow",
      "Enabled": true,
      "PermitLimit": 100,
      "WindowSeconds": 60,
      "SegmentsPerWindow": 6,
      "QueueLimit": 10,
      "AutoReplenishment": true
    }
  }
}
SettingTypeDefaultDescription
Typestring-Must be "SlidingWindow".
EnabledboolfalseEnable this policy.
PermitLimitint100Maximum requests allowed per window.
WindowSecondsint60Window duration in seconds.
SegmentsPerWindowint6Number of segments dividing the window.
QueueLimitint10Maximum queued requests when limit is reached.
AutoReplenishmentbooltrueAutomatically replenish permits.
StatusCodeintglobalOptional. HTTP status code returned when this policy rejects a request, overriding RateLimiterOptions:StatusCode. Omit to inherit the global value. See Per-Policy Status Code and Message.
StatusMessagestringglobalOptional. Response message when this policy rejects a request, overriding RateLimiterOptions:StatusMessage. Omit to inherit the global value.
PartitionobjectnullOptional Partition block for per-user / per-IP / per-header rate limiting.

See Sliding Window Limiter documentation.

Token Bucket Policy

Limits requests using the token bucket algorithm.

json
json
{
  "Policies": {
    "bucket": {
      "Type": "TokenBucket",
      "Enabled": true,
      "TokenLimit": 100,
      "TokensPerPeriod": 10,
      "ReplenishmentPeriodSeconds": 10,
      "QueueLimit": 10,
      "AutoReplenishment": true
    }
  }
}
SettingTypeDefaultDescription
Typestring-Must be "TokenBucket".
EnabledboolfalseEnable this policy.
TokenLimitint100Maximum tokens in the bucket.
TokensPerPeriodint10Number of tokens to add per replenishment period.
ReplenishmentPeriodSecondsint10How often tokens are added to the bucket.
QueueLimitint10Maximum queued requests when limit is reached.
AutoReplenishmentbooltrueAutomatically replenish tokens.
StatusCodeintglobalOptional. HTTP status code returned when this policy rejects a request, overriding RateLimiterOptions:StatusCode. Omit to inherit the global value. See Per-Policy Status Code and Message.
StatusMessagestringglobalOptional. Response message when this policy rejects a request, overriding RateLimiterOptions:StatusMessage. Omit to inherit the global value.
PartitionobjectnullOptional Partition block for per-user / per-IP / per-header rate limiting.

See Token Bucket Limiter documentation.

Concurrency Policy

Limits the number of concurrent requests.

json
json
{
  "Policies": {
    "concurrency": {
      "Type": "Concurrency",
      "Enabled": true,
      "PermitLimit": 10,
      "QueueLimit": 5,
      "OldestFirst": true
    }
  }
}
SettingTypeDefaultDescription
Typestring-Must be "Concurrency".
EnabledboolfalseEnable this policy.
PermitLimitint10Maximum concurrent requests.
QueueLimitint5Maximum queued requests when limit is reached.
OldestFirstbooltrueProcess queued requests oldest first.
StatusCodeintglobalOptional. HTTP status code returned when this policy rejects a request, overriding RateLimiterOptions:StatusCode. Omit to inherit the global value. See Per-Policy Status Code and Message.
StatusMessagestringglobalOptional. Response message when this policy rejects a request, overriding RateLimiterOptions:StatusMessage. Omit to inherit the global value.
PartitionobjectnullOptional Partition block for per-user / per-IP / per-header rate limiting.

See Concurrency Limiter documentation.

Per-User Rate Limiting (Partition)

New in 3.13.0

Rate-limiter policies can now be partitioned at request time, so each request gets its own bucket based on a value derived from HttpContext (a claim, an IP, a header, or a static fallback).

The classic use case is per-user throttling: each authenticated user gets their own quota instead of all users sharing one global bucket. Without Partition, all requests under a policy share a single global bucket.

jsonc
jsonc
"RateLimiterOptions": {
  "Enabled": true,
  "Policies": {
    "per_user": {
      "Type": "FixedWindow",
      "Enabled": true,
      "PermitLimit": 100,
      "WindowSeconds": 60,
      "Partition": {
        "Sources": [
          { "Type": "Claim", "Name": "name_identifier" },
          { "Type": "IpAddress" },
          { "Type": "Static", "Value": "anonymous" }
        ]
      }
    },
    "throttle_anon_only": {
      "Type": "FixedWindow",
      "Enabled": true,
      "PermitLimit": 10,
      "WindowSeconds": 60,
      "Partition": {
        "BypassAuthenticated": true,
        "Sources": [{ "Type": "IpAddress" }]
      }
    }
  }
}

Partition Fields

FieldTypeDefaultDescription
Sourcesarray-Ordered list of partition key sources. Walked top-to-bottom at request time; the first source returning a non-empty value wins. If no source resolves, partition resolution falls through to the literal key "unpartitioned".
BypassAuthenticatedboolfalseWhen true, signed-in users skip the limiter entirely. Evaluated before Sources, so use this for "throttle anonymous only" patterns.

Source Types

TypeBehaviorName required?
ClaimReads HttpContext.User.FindFirst(Name).Value.Yes (the claim type, e.g., "name_identifier").
IpAddressReads the client IP via HttpRequest.GetClientIpAddress(), which honors X-Forwarded-For / X-Real-IP ahead of Connection.RemoteIpAddress.No
HeaderReads HttpContext.Request.Headers[Name].Yes (the header name).
StaticAlways returns the configured Value. Useful as a terminal fallback (e.g., everyone unmatched shares the "anonymous" bucket).Uses Value instead.

Behavior is unchanged for policies without a Partition block. Each non-partitioned policy still uses a single global bucket.

Each Sources entry is validated at startup — invalid entries (e.g., Claim without Name, unknown Type) are logged at Warning and skipped. If a Partition block has no usable sources and BypassAuthenticated is false, the partition is dropped (with a Warning) and the policy reverts to a single global bucket.

Per-Policy Status Code and Message

New in 3.16.2

Each named policy can set its own StatusCode and/or StatusMessage, overriding the global RateLimiterOptions:StatusCode / RateLimiterOptions:StatusMessage for requests rejected by that policy. A policy that omits either field inherits the global value.

Previously the global StatusCode / StatusMessage were the only values returned for any rejected request, so a login-specific message (e.g. "Too many login attempts…") would be returned for every rate-limited endpoint. Now the override is resolved at rejection time from the endpoint's policy, so each policy can speak for itself:

jsonc
jsonc
"RateLimiterOptions": {
  "Enabled": true,
  "StatusCode": 429,                                  // global default
  "StatusMessage": "Too many requests. Please slow down.",
  "Policies": {
    "login_throttle": {
      "Type": "FixedWindow",
      "Enabled": true,
      "PermitLimit": 10,
      "WindowSeconds": 60,
      "StatusMessage": "Too many login attempts. Please wait a minute and try again.",
      "Partition": { "Sources": [ { "Type": "IpAddress" } ] }
    },
    "api": {
      "Type": "TokenBucket",
      "Enabled": true,
      "StatusCode": 503,
      "StatusMessage": "API capacity reached. Retry shortly."
    }
  }
}

A request rejected by login_throttle returns 429 (inherited) with the login message; a request rejected by api returns 503 with the API message. This is fully backward compatible — configs that set only the global values behave exactly as before.

Ready-to-use login_throttle policy

The shipped appsettings.json includes a disabled login_throttle policy — 10 attempts per minute partitioned per client IP, with its own rejection message — so the common case is one flag away:

jsonc
jsonc
"login_throttle": {
  "Type": "FixedWindow",
  "Enabled": false,
  "PermitLimit": 10,
  "WindowSeconds": 60,
  "QueueLimit": 0,
  "AutoReplenishment": true,
  "StatusMessage": "Too many login attempts. Please wait a minute and try again.",
  "Partition": { "Sources": [ { "Type": "IpAddress" } ], "BypassAuthenticated": false }
}

Set "Enabled": true and apply it to a login endpoint with the rate_limiter_policy annotation (rate_limiter login_throttle), or set it as DefaultPolicy.

Complete Example

Configuration with multiple policies:

json
json
{
  "RateLimiterOptions": {
    "Enabled": true,
    "StatusCode": 429,
    "StatusMessage": "Too many requests. Please try again later.",
    "DefaultPolicy": "bucket",
    "Policies": {
      "fixed": {
        "Type": "FixedWindow",
        "Enabled": true,
        "PermitLimit": 100,
        "WindowSeconds": 60,
        "QueueLimit": 10,
        "AutoReplenishment": true
      },
      "sliding": {
        "Type": "SlidingWindow",
        "Enabled": true,
        "PermitLimit": 100,
        "WindowSeconds": 60,
        "SegmentsPerWindow": 6,
        "QueueLimit": 10,
        "AutoReplenishment": true
      },
      "bucket": {
        "Type": "TokenBucket",
        "Enabled": true,
        "TokenLimit": 100,
        "TokensPerPeriod": 10,
        "ReplenishmentPeriodSeconds": 10,
        "QueueLimit": 10,
        "AutoReplenishment": true
      },
      "concurrency": {
        "Type": "Concurrency",
        "Enabled": true,
        "PermitLimit": 10,
        "QueueLimit": 5,
        "OldestFirst": true
      },
      "per_user": {
        "Type": "FixedWindow",
        "Enabled": true,
        "PermitLimit": 100,
        "WindowSeconds": 60,
        "QueueLimit": 10,
        "AutoReplenishment": true,
        "Partition": {
          "Sources": [
            { "Type": "Claim", "Name": "name_identifier" },
            { "Type": "IpAddress" },
            { "Type": "Static", "Value": "anonymous" }
          ]
        }
      }
    }
  }
}

Next Steps

  • Server & SSL - Configure HTTPS and Kestrel web server
  • CORS - Configure Cross-Origin Resource Sharing

See Also

Comments