-
Notifications
You must be signed in to change notification settings - Fork 376
Add implementing-rate-limiting skill #131
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,213 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ```skill | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is wrapped in skill markdown block |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| --- | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name: implementing-rate-limiting | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| description: Implement .NET 7+ built-in rate limiting middleware with correct algorithm selection, partitioning, and response handling. Use when adding API rate limiting without a third-party library. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
this gives it more "keywords" to help activate it appropriately
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the use/do not use below can now be removed as it's all here |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| --- | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1
to
+6
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Implementing Rate Limiting in ASP.NET Core (.NET 7+) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ## When to Use | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. move when to use/not use into description to enable lazy loading. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - Adding rate limiting to ASP.NET Core APIs using the built-in middleware | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - Choosing between fixed window, sliding window, token bucket, and concurrency limiter | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - Configuring per-client/per-endpoint rate limits | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - Fixing rate limiting that silently does nothing or blocks the wrong requests | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ## When Not to Use | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - Distributed rate limiting across multiple server instances (need Redis-backed like `AspNetCoreRateLimit` or a gateway) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - Rate limiting at the API gateway/reverse proxy layer (YARP, nginx, Azure API Management) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - Pre-.NET 7 projects (no built-in support) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ## Inputs | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| | Input | Required | Description | | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| |-------|----------|-------------| | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| | API endpoints to protect | Yes | Which routes need rate limiting | | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| | Rate limit requirements | Yes | Requests per window, per-client vs global | | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| | .NET version | No | Must be .NET 7+ for built-in support | | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ## Workflow | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ### Step 1: Choose the right algorithm | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| | Algorithm | Best For | Behavior | | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| |-----------|----------|----------| | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| | **Fixed Window** | Simple per-minute/per-hour limits | Counter resets at window boundary. ⚠️ Burst problem: 100 req at end of window + 100 at start of next = 200 in 1 second | | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| | **Sliding Window** | Smoother rate distribution | Divides window into segments, slides across time. Avoids burst problem | | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| | **Token Bucket** | Allowing controlled bursts | Tokens replenish at fixed rate, requests consume tokens. Good for APIs that should allow short bursts | | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| | **Concurrency Limiter** | Limiting simultaneous requests | Caps concurrent in-flight requests, not rate. Good for protecting expensive endpoints | | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| **Common mistake:** Using Fixed Window when you need smooth distribution. A fixed window of "100 per minute" allows 200 requests in 2 seconds if they straddle the window boundary. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ### Step 2: Configure the rate limiter in Program.cs | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ```csharp | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| using Microsoft.AspNetCore.RateLimiting; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| using System.Threading.RateLimiting; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| builder.Services.AddRateLimiter(options => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // CRITICAL: Set rejection status code — default is 503! Most APIs should use 429 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| options.RejectionStatusCode = StatusCodes.Status429TooManyRequests; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Global rate limiter: fixed window | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(context => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return RateLimitPartition.GetFixedWindowLimiter( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| partitionKey: context.Connection.RemoteIpAddress?.ToString() ?? "unknown", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| factory: _ => new FixedWindowRateLimiterOptions | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| PermitLimit = 100, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Window = TimeSpan.FromMinutes(1), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| QueueProcessingOrder = QueueProcessingOrder.OldestFirst, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| QueueLimit = 0 // Reject immediately, don't queue | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Named policy: sliding window for sensitive endpoints | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| options.AddSlidingWindowLimiter("api-sensitive", slidingOptions => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| slidingOptions.PermitLimit = 10; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| slidingOptions.Window = TimeSpan.FromMinutes(1); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| slidingOptions.SegmentsPerWindow = 6; // 10-second segments | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| slidingOptions.QueueLimit = 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+54
to
+74
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Global rate limiter: fixed window | |
| options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(context => | |
| { | |
| return RateLimitPartition.GetFixedWindowLimiter( | |
| partitionKey: context.Connection.RemoteIpAddress?.ToString() ?? "unknown", | |
| factory: _ => new FixedWindowRateLimiterOptions | |
| { | |
| PermitLimit = 100, | |
| Window = TimeSpan.FromMinutes(1), | |
| QueueProcessingOrder = QueueProcessingOrder.OldestFirst, | |
| QueueLimit = 0 // Reject immediately, don't queue | |
| }); | |
| }); | |
| // Named policy: sliding window for sensitive endpoints | |
| options.AddSlidingWindowLimiter("api-sensitive", slidingOptions => | |
| { | |
| slidingOptions.PermitLimit = 10; | |
| slidingOptions.Window = TimeSpan.FromMinutes(1); | |
| slidingOptions.SegmentsPerWindow = 6; // 10-second segments | |
| slidingOptions.QueueLimit = 0; | |
| // Global rate limiter: sliding window for smoother distribution across clients | |
| options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(context => | |
| { | |
| return RateLimitPartition.GetSlidingWindowLimiter( | |
| partitionKey: context.Connection.RemoteIpAddress?.ToString() ?? "unknown", | |
| factory: _ => new SlidingWindowRateLimiterOptions | |
| { | |
| PermitLimit = 100, | |
| Window = TimeSpan.FromMinutes(1), | |
| SegmentsPerWindow = 6, // 10-second segments | |
| QueueProcessingOrder = QueueProcessingOrder.OldestFirst, | |
| QueueLimit = 0 // Reject immediately, don't queue | |
| }); | |
| }); | |
| // Named policy: token bucket for sensitive endpoints that can tolerate short bursts | |
| options.AddTokenBucketLimiter("api-sensitive", tokenOptions => | |
| { | |
| tokenOptions.TokenLimit = 10; // max burst size | |
| tokenOptions.TokensPerPeriod = 10; | |
| tokenOptions.ReplenishmentPeriod = TimeSpan.FromMinutes(1); | |
| tokenOptions.AutoReplenishment = true; | |
| tokenOptions.QueueProcessingOrder = QueueProcessingOrder.OldestFirst; | |
| tokenOptions.QueueLimit = 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this line needed given its set by default above to 429?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider moving after UseAuthorization so you can rate limit based on user information.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess you need using System.Security.Claims;
Copilot
AI
Feb 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The per-user partitioning snippet uses ClaimTypes.NameIdentifier but doesn’t include the required using System.Security.Claims; (and it isn’t referenced earlier in the shown usings). To keep the snippet copy/pasteable, add that using (or fully-qualify System.Security.Claims.ClaimTypes).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code above uses RemoteIpAddress
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,29 @@ | ||||||||||||||
| scenarios: | ||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. seems like we should have more than 1 scenario to cover more of the skill as there's several different directions to go. |
||||||||||||||
| - name: "Add per-client rate limiting to an ASP.NET Core API" | ||||||||||||||
| prompt: | | ||||||||||||||
| I need to add rate limiting to my ASP.NET Core 8 API. Requirements: | ||||||||||||||
| 1. Global limit: 100 requests per minute per IP address | ||||||||||||||
| 2. Stricter limit on POST /api/orders: 10 requests per minute per authenticated user | ||||||||||||||
| 3. No limit on GET /healthz | ||||||||||||||
| 4. Return a proper 429 response with Retry-After header when rate limited | ||||||||||||||
|
|
||||||||||||||
| Show me the full Program.cs configuration and how to apply it to the endpoints. | ||||||||||||||
| assertions: | ||||||||||||||
| - type: "output_matches" | ||||||||||||||
| pattern: "(AddRateLimiter|UseRateLimiter)" | ||||||||||||||
| - type: "output_matches" | ||||||||||||||
| pattern: "(429|TooManyRequests)" | ||||||||||||||
| - type: "output_matches" | ||||||||||||||
| pattern: "(RequireRateLimiting|EnableRateLimiting)" | ||||||||||||||
| - type: "output_matches" | ||||||||||||||
| pattern: "(DisableRateLimiting|healthz)" | ||||||||||||||
|
||||||||||||||
| pattern: "(DisableRateLimiting|healthz)" | |
| pattern: "DisableRateLimiting" | |
| - type: "output_matches" | |
| pattern: "healthz" | |
| - type: "output_matches" | |
| pattern: "(Retry-After|MetadataName.RetryAfter)" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
add CODEOWNERS entry
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
and update to match directory structure in main