Skip to content

Add implementing-rate-limiting skill - #131

Closed
mrsharm wants to merge 1 commit into
dotnet:mainfrom
mrsharm:musharm/implementing-rate-limiting
Closed

Add implementing-rate-limiting skill#131
mrsharm wants to merge 1 commit into
dotnet:mainfrom
mrsharm:musharm/implementing-rate-limiting

Conversation

@mrsharm

@mrsharm mrsharm commented Feb 26, 2026

Copy link
Copy Markdown
Member

Skill: implementing-rate-limiting

Teaches the model to implement .NET 8 built-in rate limiting correctly, covering:

  • Return 429 (not default 503) via RateLimiterOptions.RejectionStatusCode = 429`n- Middleware ordering (UseRateLimiter() after UseRouting(), before MapControllers())
  • Per-client partitioned rate limiting with CreatePartitionedLimiter()"
  • Named policies applied per-endpoint
  • OnRejected callback with Retry-After header
  • Algorithm selection (fixed window, sliding window, token bucket, concurrency)

Eval Results (3-run)

Overall: +39.4% improvement - PASSED

Scenario BL SK
Add per-client rate limiting 3 4

Copilot AI review requested due to automatic review settings February 26, 2026 17:12
@mrsharm

mrsharm commented Feb 26, 2026

Copy link
Copy Markdown
Member Author

Eval Results (3-run validation)

Model: claude-opus-4.6 (baseline & skill)
Overall: +39.4% improvement - PASSED

Scenario: Add per-client rate limiting to an ASP.NET Core API

Run Baseline With Skill Delta
1 3 4 +1
2 3 4 +1
3 3 4 +1

Key Improvements

  • Baseline consistently scores 3/5 (misses 429 status code, middleware ordering, Retry-After header)
  • Skill consistently raises to 4/5 (adds proper rejection status, middleware placement, OnRejected callback)
  • 100% consistency across all 3 runs

1-run screening

  • Score: +15.3% (BL=3, SK=4) - PASSED

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new .NET skill and evaluation scenario focused on implementing ASP.NET Core’s built-in rate limiting correctly (status codes, middleware ordering, partitioning, policies, and rejection handling).

Changes:

  • Added a new eval scenario to validate responses include key rate-limiting configuration concepts (policies, 429 responses, endpoint opt-out).
  • Added a new implementing-rate-limiting skill document describing algorithms, configuration, and common pitfalls.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
src/dotnet/tests/implementing-rate-limiting/eval.yaml New evaluation scenario for per-client + per-endpoint rate limiting requirements.
src/dotnet/skills/implementing-rate-limiting/SKILL.md New skill content covering built-in rate limiting setup, middleware ordering, and partitioning patterns.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1 to +6
```skill
---
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.
---

Copilot AI Feb 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The file is wrapped in an outer fenced code block (skill ... ), which prevents the YAML frontmatter from being detected/stripped (SkillProfiler expects frontmatter to start at the beginning of the file with ---). It also makes the entire document a single code block, so the internal csharp fences won’t render as code blocks. Remove the outer skill wrapper and start the file directly with the --- frontmatter (like other skills in this repo).

Copilot uses AI. Check for mistakes.
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;

Copilot AI Feb 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This section emphasizes fixed window’s burst problem, but the example immediately configures the global limiter as a fixed window. That inconsistency can steer readers toward the exact pitfall the skill warns about and may conflict with the eval rubric’s preference for sliding window/token bucket. Consider making the global limiter sliding window (or token bucket) in the main example, or explicitly justify why fixed window is acceptable for the global policy here.

Suggested change
// 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;

Copilot uses AI. Check for mistakes.
Comment on lines +163 to +166
options.AddPolicy("per-user", context =>
{
var userId = context.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;

Copilot AI Feb 26, 2026

Copy link

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).

Copilot uses AI. Check for mistakes.
- type: "output_matches"
pattern: "(RequireRateLimiting|EnableRateLimiting)"
- type: "output_matches"
pattern: "(DisableRateLimiting|healthz)"

Copilot AI Feb 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The health-check assertion is currently too permissive: (DisableRateLimiting|healthz) will pass if the model merely mentions healthz without actually disabling rate limiting. Consider splitting this into two assertions (one requiring DisableRateLimiting, one requiring healthz), and add an assertion that checks for setting a Retry-After header (e.g., Retry-After or MetadataName.RetryAfter) since that’s a stated requirement/rubric item.

Suggested change
pattern: "(DisableRateLimiting|healthz)"
pattern: "DisableRateLimiting"
- type: "output_matches"
pattern: "healthz"
- type: "output_matches"
pattern: "(Retry-After|MetadataName.RetryAfter)"

Copilot uses AI. Check for mistakes.
@adityamandaleeka

Copy link
Copy Markdown
Member

@BrennanConroy


# Implementing Rate Limiting in ASP.NET Core (.NET 7+)

## When to Use

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move when to use/not use into description to enable lazy loading.

@@ -0,0 +1,213 @@
```skill

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add CODEOWNERS entry

Copy link
Copy Markdown
Contributor

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

@@ -0,0 +1,213 @@
```skill

@danmoseley danmoseley Mar 2, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is wrapped in skill markdown block
this is wrong I think - remove it.

### Step 4: Per-user/per-tenant partitioning

```csharp
// Per-authenticated-user rate limit

Copy link
Copy Markdown
Contributor

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;

// Custom response for rejected requests
options.OnRejected = async (context, cancellationToken) =>
{
context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;

Copy link
Copy Markdown
Contributor

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?

@@ -0,0 +1,29 @@
scenarios:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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.

```skill
---
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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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.
description: Implement .NET 7+ built-in rate limiting (Microsoft.AspNetCore.RateLimiting / System.Threading.RateLimiting) with correct algorithm selection, partitioning, and response handling. Use when adding API rate limiting to ASP.NET Core without a third-party library. Covers fixed window, sliding window, token bucket, and concurrency limiter; middleware ordering; 429 status codes; per-client partitioning; and Retry-After headers. Not for distributed/multi-instance rate limiting (use AspNetCoreRateLimit with Redis or a gateway instead), gateway-layer limiting (YARP, nginx, Azure API Management), or pre-.NET 7 projects.

this gives it more "keywords" to help activate it appropriately

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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

// Rate limiting middleware MUST be after routing but before endpoint execution
// WRONG order will cause it to silently not apply to endpoints
app.UseRouting(); // must come first
app.UseRateLimiter(); // ← HERE — after UseRouting, before MapControllers/MapGet

Copy link
Copy Markdown
Member

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.

| Missing `RequireRateLimiting()` on endpoints | Global limiter works but named policies do nothing | Apply policies to endpoints explicitly |
| `AutoReplenishment = false` on token bucket | Tokens never replenish, all requests rejected after initial burst | Set `AutoReplenishment = true` (or use a background timer) |
| `QueueLimit > 0` without timeout | Requests queue indefinitely under sustained overload | Set `QueueLimit = 0` to reject immediately, or impose a timeout |
| Partitioning by `RemoteIpAddress` behind proxy | All requests share one IP (the proxy) | Partition by `X-Forwarded-For` header or authenticated user |

Copy link
Copy Markdown
Member

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

@mrsharm

mrsharm commented Mar 6, 2026

Copy link
Copy Markdown
Member Author

Closing: replaced by new PR from mrsharm/skills with plugins/ directory structure.

@mrsharm mrsharm closed this Mar 6, 2026
mrsharm added a commit to mrsharm/skills that referenced this pull request Mar 24, 2026
- Fix X-Forwarded-For spoofing: use RemoteIpAddress with ForwardedHeaders guidance
- Fix Headers.RetryAfter to Headers["Retry-After"] indexer
- Remove redundant StatusCode=429 in OnRejected (already set via RejectionStatusCode)
- Split health-check assertion into two (DisableRateLimiting + /healthz)
- Remove brittle expect_tools: ["bash"]
- Update proxy partitioning guidance in mistakes table and validation checklist
- Add CODEOWNERS entry for aspnetcore plugin
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants