MCPInstall

rate-limiting

Rate limiting patterns for ASP.NET Core Razor Pages applications. Covers fixed window, sliding window, token bucket algorithms, and distributed rate limiting with Redis. Use when implementing rate limiting in ASP.NET Core applications, choosing between different rate limiting algorithms, or setting up distributed rate limiting with Redis.

Installs
85
Last 24h
—
Trending rank
—
Security
Not audited

Install

Works with every agent the skills CLI supports.

npx skills add https://github.com/wshaddix/dotnet-skills --skill rate-limiting
  1. 01

    Install

    Run the command in your project. The CLI asks which agents to install for.

  2. 02

    Describe the task

    Ask your agent for something this skill covers. It loads rate-limiting automatically when relevant.

  3. 03

    Review the result

    The agent follows the SKILL.md steps, scripts and references bundled with the skill.

SKILL.md

The instructions your agent reads when the skill is triggered.

Rationale

Rate limiting protects applications from abuse, ensures fair resource usage, and prevents cascading failures during traffic spikes. Without proper rate limiting, APIs can be overwhelmed by malicious or accidental high-volume requests, leading to degraded performance or outages. These patterns provide production-ready approaches to request throttling in ASP.NET Core applications.

Patterns

Pattern 1: Built-in Rate Limiting Middleware (.NET 7+)

Use the built-in Microsoft.AspNetCore.RateLimiting middleware for common scenarios.

// Program.cs - Basic rate limiting configuration
builder.Services.AddRateLimiter(options =>
{
    // Global rate limit for all requests
    options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(
        httpContext =>
        {
            var clientId = httpContext.User.Identity?.Name ?? 
                          httpContext.Connection.RemoteIpAddress?.ToString() ?? 
                          "anonymous";
            
            return RateLimitPartition.GetFixedWindowLimiter(
                partitionKey: clientId,
                factory: _ => new FixedWindowRateLimiterOptions
                {
                    PermitLimit = 100,
                    Window = TimeSpan.FromMinutes(1),
                    QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
                    QueueLimit = 2
                });
        });

    // Named policies for different endpoints
    options.AddFixedWindowLimiter("login", opt =>
    {
        opt.PermitLimit = 5;
        opt.Window = TimeSpan.FromMinutes(5);
        opt.QueueLimit = 0; // Don't queue login requests
    });

    options.AddFixedWindowLimiter("api", opt =>
    {
        opt.PermitLimit = 1000;
        opt.Window = TimeSpan.FromMinutes(1);
    });

    options.AddSlidingWindowLimiter("strict", opt =>
    {
        opt.PermitLimit = 10;
        opt.Window = TimeSpan.FromSeconds(10);
        opt.SegmentsPerWindow = 2;
    });

    options.AddTokenBucketLimiter("burst", opt =>
    {
        opt.TokenLimit = 100;
        opt.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
        opt.QueueLimit = 5;
        opt.ReplenishmentPeriod = TimeSpan.FromSeconds(10);
        opt.TokensPerPeriod = 20;
        opt.AutoReplenishment = true;
    });

    options.AddConcurrencyLimiter("concurrent", opt =>
    {
        opt.PermitLimit = 10;
        opt.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
        opt.QueueLimit = 5;
    });

    // Custom rejection response
    options.OnRejected = async (context, token) =>
    {
        context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
        context.HttpContext.Response.Headers.Append("Retry-After", "60");
        
        await context.HttpContext.Response.WriteAsJsonAsync(new
        {
            Error = "Rate limit exceeded. Please try again later.",
            RetryAfter = 60
        }, token);
    };
});

// Middleware placement (must be after UseRouting, before UseEndpoints)
var app = builder.Build();
app.UseRouting();
app.UseRateLimiter(); // Enable rate limiting
app.MapControllers();
app.MapRazorPages();

Pattern 2: Per-Endpoint Rate Limiting

Apply different rate limits to different endpoints using attributes or endpoint configuration.

// Using EnableRateLimiting attribute on controllers
[ApiController]
[Route("api/[controller]")]
[EnableRateLimiting("api")] // Use named policy
public class ProductsController : ControllerBase
{
    [HttpGet]
    public async Task<IActionResult> GetAll()
    {
        // Limited by "api" policy (1000 requests/minute)
        return Ok();
    }

    [HttpPost]
    [EnableRateLimiting("strict")] // Override with stricter policy
    public async Task<IActionResult> Create([FromBody] ProductDto dto)
    {
        // Limited by "strict" policy (10 requests/10 seconds)
        return Created();
    }
}

// Razor Pages with rate limiting
public class LoginModel : PageModel
{
    // Page is rate limited via attribute
    [RateLimitPolicy("login")]
    public async Task<IActionResult> OnPostAsync()
    {
        // Login logic - protected by login policy (5 attempts per 5 minutes)
    }
}

// Endpoint-specific configuration in Program.cs
app.MapPost("/api/login", async (LoginRequest request) =>
{
    // Login logic
})
.AddEndpointFilter<RateLimitEndpointFilter>()
.RequireRateLimiting("login");

// Disable rate limiting for specific endpoints
app.MapGet("/health", () => Results.Ok())
    .DisableRateLimiting();

Pattern 3: Redis-Based Distributed Rate Limiting

Use Redis for rate limiting in distributed/multi-server environments.

// Redis rate limiting configuration
builder.Services.AddRateLimiter(options =>
{
    options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(
        httpContext =>
        {
            var clientId = GetClientIdentifier(httpContext);
            
            return RateLimitPartition.GetFixedWindowLimiter(
                partitionKey: clientId,
                factory: partitionKey => new FixedWindowRateLimiterOptions
                {
                    PermitLimit = 100,
                    Window = TimeSpan.FromMinutes(1)
                });
        });
});

// Custom distributed rate limiter using Redis
public class RedisRateLimiter : IRateLimiter
{
    private readonly IConnectionMultiplexer _redis;
    private readonly ILogger<RedisRateLimiter> _logger;

    public RedisRateLimiter(IConnectionMultiplexer redis, ILogger<RedisRateLimiter> logger)
    {
        _redis = redis;
        _logger = logger;
    }

    public async Task<RateLimitResult> CheckLimitAsync(
        string key, 
        int limit, 
        TimeSpan window)
    {
        var db = _redis.GetDatabase();
        var redisKey = $"ratelimit:{key}";
        
        // Lua script for atomic check-and-increment
        var script = @"
            local current = redis.call('GET', KEYS[1])
            if current == false then
                current = 0
            end
            if tonumber(current) < tonumber(ARGV[1]) then
                redis.call('INCR', KEYS[1])
                redis.call('EXPIRE', KEYS[1], ARGV[2])
                return {1, tonumber(current) + 1, tonumber(ARGV[1])}
            else
                local ttl = redis.call('TTL', KEYS[1])
                return {0, tonumber(current), tonumber(ARGV[1]), ttl}
            end";

        var result = await db.ScriptEvaluateAsync(script,
            new RedisKey[] { redisKey },
            new RedisValue[] { limit, window.TotalSeconds });

        var values = (RedisResult[])result!;
        var allowed = (bool)values[0];
        var current = (int)values[1];
        var limitValue = (int)values[2];
        var retryAfter = allowed ? 0 : (int)values[3];

        return new RateLimitResult(
            Allowed: allowed,
            Current: current,
            Limit: limitValue,
            RetryAfter: retryAfter);
    }
}

public record RateLimitResult(bool Allowed, int Current, int Limit, int RetryAfter);

// Custom rate limiting middleware
public class DistributedRateLimitMiddleware
{
    private readonly RequestDelegate _next;
    private readonly RedisRateLimiter _rateLimiter;
    private readonly ILogger<DistributedRateLimitMiddleware> _logger;

    public DistributedRateLimitMiddleware(
        RequestDelegate next,
        RedisRateLimiter rateLimiter,
        ILogger<DistributedRateLimitMiddleware> logger)
    {
        _next = next;
        _rateLimiter = rateLimiter;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var clientId = GetClientIdentifier(context);
        var path = context.Request.Path.Value ?? "";
        
        // Different limits for different paths
        var (limit, window) = GetLimitForPath(path);
        
        var result = await _rateLimiter.CheckLimitAsync(
            $"{clientId}:{path}", 
            limit, 
            window);

        // Add rate limit headers
        AddRateLimitHeaders(context.Response, result);

        if (!result.Allowed)
        {
            _logger.LogWarning(
                "Rate limit exceeded for {ClientId} on {Path}",
                clientId, path);

            context.Response.StatusCode = StatusCodes.Status429TooManyRequests;
            context.Response.Headers.Append("Retry-After", result.RetryAfter.ToString());
            
            await context.Response.WriteAsJsonAsync(new
            {
                Error = "Rate limit exceeded",
                RetryAfter = result.RetryAfter,
                Limit = result.Limit,
                Window = window.TotalSeconds
            });
            
            return;
        }

        await _next(context);
    }

    private static (int Limit, TimeSpan Window) GetLimitForPath(string path)
    {
        if (path.StartsWith("/api/login"))
            return (5, TimeSpan.FromMinutes(5));
        if (path.StartsWith("/api/"))
            return (1000, TimeSpan.FromMinutes(1));
        
        return (100, TimeSpan.FromMinutes(1));
    }

    private static void AddRateLimitHeaders(HttpResponse response, RateLimitResult result)
    {
        response.Headers.Append("X-RateLimit-Limit", result.Limit.ToString());
        response.Headers.Append("X-RateLimit-Remaining", (result.Limit - result.Current).ToString());
    }
}

Pattern 4: User-Based Rate Limiting

Implement rate limiting based on authenticated user identity.

// User-based rate limiter
public class UserBasedRateLimiter
{
    private readonly IRateLimiter _rateLimiter;
    private readonly IUserService _userService;

    public UserBasedRateLimiter(IRateLimiter rateLimiter, IUserService userService)
    {
        _rateLimiter = rateLimiter;
        _userService = userService;
    }

    public async Task<bool> CheckUserLimitAsync(HttpContext context)
    {
        var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
        
        if (string.IsNullOrEmpty(userId))
        {
            // Fall back to IP-based limiting for anonymous users
            return await CheckAnonymousLimitAsync(context);
        }

        // Get user's subscription tier
        var user = await _userService.GetUserAsync(userId);
        var (limit, window) = GetLimitForTier(user?.SubscriptionTier);

        var key = $"user:{userId}";
        var result = await _rateLimiter.CheckLimitAsync(key, limit, window);

        AddRateLimitHeaders(context.Response, result);
        
        return result.Allowed;
    }

    private async Task<bool> CheckAnonymousLimitAsync(HttpContext context)
    {
        var ipAddress = context.Connection.RemoteIpAddress?.ToString() ?? "unknown";
        var key = $"ip:{ipAddress}";
        
        // Stricter limits for anonymous users
        var result = await _rateLimiter.CheckLimitAsync(key, 30, TimeSpan.FromMinutes(1));
        
        AddRateLimitHeaders(context.Response, result);
        
        return result.Allowed;
    }

    private static (int Limit, TimeSpan Window) GetLimitForTier(SubscriptionTier? tier)
    {
        return tier switch
        {
            SubscriptionTier.Enterprise => (10000, TimeSpan.FromMinutes(1)),
            SubscriptionTier.Pro => (1000, TimeSpan.FromMinutes(1)),
            SubscriptionTier.Basic => (100, TimeSpan.FromMinutes(1)),
            _ => (50, TimeSpan.FromMinutes(1)) // Free tier
        };
    }
}

// Middleware integration
public class UserRateLimitMiddleware
{
    private readonly RequestDelegate _next;
    private readonly UserBasedRateLimiter _rateLimiter;

    public UserRateLimitMiddleware(RequestDelegate next, UserBasedRateLimiter rateLimiter)
    {
        _next = next;
        _rateLimiter = rateLimiter;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        if (!await _rateLimiter.CheckUserLimitAsync(context))
        {
            context.Response.StatusCode = StatusCodes.Status429TooManyRequests;
            await context.Response.WriteAsJsonAsync(new
            {
                Error = "Rate limit exceeded",
                UpgradeUrl = "/pricing"
            });
            return;
        }

        await _next(context);
    }
}

// Razor Page with tier-based limiting
public class ApiDashboardModel : PageModel
{
    private readonly IUserRateLimitService _rateLimitService;

    public int CurrentUsage { get; set; }
    public int MonthlyLimit { get; set; }

    public async Task OnGetAsync()
    {
        var userId = User.FindFirstValue(ClaimTypes.NameIdentifier)!;
        
        var usage = await _rateLimitService.GetMonthlyUsageAsync(userId);
        CurrentUsage = usage.Current;
        MonthlyLimit = usage.Limit;
    }
}

Pattern 5: Rate Limiting with Client Identification

Handle various client identification scenarios including proxies and load balancers.

public static class ClientIdentifierHelper
{
    public static string GetClientIdentifier(HttpContext context)
    {
        // 1. Try authenticated user first
        var userId = context.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
        if (!string.IsNullOrEmpty(userId))
        {
            return $"user:{userId}";
        }

        // 2. Try API key
        var apiKey = context.Request.Headers["X-API-Key"].FirstOrDefault();
        if (!string.IsNullOrEmpty(apiKey))
        {
            return $"apikey:{apiKey}";
        }

        // 3. Get IP address (handling proxies)
        var ip = GetClientIpAddress(context);
        return $"ip:{ip}";
    }

    public static string GetClientIpAddress(HttpContext context)
    {
        // Check X-Forwarded-For header (when behind load balancer/proxy)
        var forwardedFor = context.Request.Headers["X-Forwarded-For"].FirstOrDefault();
        if (!string.IsNullOrEmpty(forwardedFor))
        {
            // Take the first IP if multiple are present
            var ips = forwardedFor.Split(',', StringSplitOptions.RemoveEmptyEntries);
            if (ips.Length > 0)
            {
                return ips[0].Trim();
            }
        }

        // Check X-Real-IP header
        var realIp = context.Request.Headers["X-Real-IP"].FirstOrDefault();
        if (!string.IsNullOrEmpty(realIp))
        {
            return realIp;
        }

        // Fall back to connection IP
        return context.Connection.RemoteIpAddress?.ToString() ?? "unknown";
    }

    public static bool IsTrustedProxy(HttpContext context, IEnumerable<string> trustedProxies)
    {
        var remoteIp = context.Connection.RemoteIpAddress;
        return remoteIp != null && trustedProxies.Any(proxy =>
        {
            if (IPAddress.TryParse(proxy, out var trustedIp))
            {
                return remoteIp.Equals(trustedIp);
            }
            return false;
        });
    }
}

// Configuration for forwarded headers (Program.cs)
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
    options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
    options.KnownNetworks.Clear();
    options.KnownProxies.Clear();
});

// Use forwarded headers middleware
app.UseForwardedHeaders();

Anti-Patterns

// ❌ BAD: Same limits for all endpoints
options.AddFixedWindowLimiter("default", opt =>
{
    opt.PermitLimit = 100;
    opt.Window = TimeSpan.FromMinutes(1);
});
// Applied to everything - login endpoints need stricter limits!

// ✅ GOOD: Different policies for different endpoints
options.AddFixedWindowLimiter("login", opt =>
{
    opt.PermitLimit = 5; // Strict for authentication
    opt.Window = TimeSpan.FromMinutes(5);
});

options.AddFixedWindowLimiter("api", opt =>
{
    opt.PermitLimit = 1000; // Generous for API
    opt.Window = TimeSpan.FromMinutes(1);
});

// ❌ BAD: No headers indicating rate limit status
// Clients can't track their usage

// ✅ GOOD: Include rate limit headers
context.Response.Headers.Append("X-RateLimit-Limit", limit.ToString());
context.Response.Headers.Append("X-RateLimit-Remaining", remaining.ToString());
context.Response.Headers.Append("X-RateLimit-Reset", resetTime.ToString());

// ❌ BAD: Wrong middleware order
app.UseRateLimiter();
app.UseAuthentication();
// Can't identify users if auth hasn't run yet!

// ✅ GOOD: Rate limiter after authentication
app.UseAuthentication();
app.UseAuthorization();
app.UseRateLimiter();

// ❌ BAD: Not handling rate limit in-memory only
// Won't work across multiple servers
var limiter = new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions
{
    PermitLimit = 100,
    Window = TimeSpan.FromMinutes(1)
});

// ✅ GOOD: Use distributed storage for multi-server
typeof(DistributedCacheRateLimiter)

// ❌ BAD: No fallback when rate limiter fails
public async Task<bool> CheckLimit(string key)
{
    var result = await _redis.CheckLimitAsync(key); // If Redis fails, whole app fails!
    return result.Allowed;
}

// ✅ GOOD: Graceful degradation
public async Task<bool> CheckLimit(string key)
{
    try
    {
        var result = await _redis.CheckLimitAsync(key);
        return result.Allowed;
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Rate limit check failed, allowing request");
        return true; // Fail open
    }
}

// ❌ BAD: Blocking on rate limit check
public IActionResult GetData()
{
    var allowed = CheckLimitAsync().Result; // Blocks thread!
    if (!allowed) return StatusCode(429);
    // ...
}

// ✅ GOOD: Async rate limiting
public async Task<IActionResult> GetDataAsync()
{
    var allowed = await CheckLimitAsync();
    if (!allowed) return StatusCode(429);
    // ...
}

// ❌ BAD: Logging every blocked request at Error level
// Creates log spam during attacks

// ✅ GOOD: Log at appropriate level with sampling
_logger.LogWarning("Rate limit exceeded for {ClientId}", clientId);

// Or use metrics instead
_metrics.RecordRateLimitHit(clientId);

References

FAQ

What does the rate-limiting skill do?

Rate limiting patterns for ASP.NET Core Razor Pages applications. Covers fixed window, sliding window, token bucket algorithms, and distributed rate limiting with Redis. Use when implementing rate limiting in ASP.NET Core applications, choosing between different rate limiting algorithms, or setting up distributed rate limiting with Redis.

How do I install rate-limiting?

Run npx skills add https://github.com/wshaddix/dotnet-skills --skill rate-limiting in your project. The skills CLI detects the agents you use and copies the skill into the right folder. For Claude Code you can also place the skill folder at ~/.claude/skills/rate-limiting/ (personal) or .claude/skills/rate-limiting/ (project).

Which AI agents can use rate-limiting?

Any agent that supports the open Agent Skills format (a folder with a SKILL.md file), including Claude Code, Codex, Cursor, GitHub Copilot, Gemini CLI, Windsurf, Cline, OpenCode and more.

Is rate-limiting safe to install?

No partner security audit is available yet. Skills can run commands and scripts, so read the SKILL.md and any bundled files before installing.

Who maintains rate-limiting?

It is published by wshaddix in wshaddix/dotnet-skills. It has 85 recorded installs.

More from wshaddix/dotnet-skills

View all →

dotnet-xunit

wshaddix/dotnet-skills

Writing xUnit tests. v3 Fact/Theory, fixtures, parallelism, IAsyncLifetime, v2 compatibility.

Backend & APIs613 installs

dotnet-testing-strategy

wshaddix/dotnet-skills

Deciding how to test .NET code. Unit vs integration vs E2E decision tree, test doubles.

Testing & QA610 installs

modern-csharp-coding-standards

wshaddix/dotnet-skills

Write modern, high-performance C code using records, pattern matching, value objects, async/await, Span /Memory , and best-practice API design patterns. Emphasizes functional-style programming with C 12+ features. Use when writing new C code or refactoring existing code, designing public APIs for libraries or services, optimizing performance-critical code paths, or building async/await-heavy applications.

csharp-wolverinefx

wshaddix/dotnet-skills

Build .NET applications with WolverineFX for messaging, HTTP services, and event sourcing. Use when implementing command handlers, message handlers, HTTP endpoints with WolverineFx.HTTP, transactional outbox patterns, event sourcing with Marten, CQRS architectures, cascading messages, batch message processing, or configuring transports like RabbitMQ, Azure Service Bus, or Amazon SQS.

Backend & APIs570 installs

testcontainers

wshaddix/dotnet-skills

Patterns for using Testcontainers in .NET integration tests to spin up real dependencies like databases and message queues. Use when writing integration tests that require real databases, testing with message brokers like RabbitMQ or Kafka, or isolating test dependencies with Docker containers.

Testing & QA565 installs

Dependency injection patterns and best practices using Microsoft.Extensions.DependencyInjection for .NET applications. Use when configuring DI containers in .NET, choosing between service lifetimes (Singleton, Scoped, Transient), or implementing decorator patterns and service interception.

Security549 installs

Popular data & databases skills

View category →

Postgres best practices maintained by Supabase, for Postgres running anywhere. Load this skill BEFORE writing or changing anything that lives in a Postgres database: creating or altering tables and columns (including choosing column types), schema design, migrations and declarative schema files, RLS policies and the tests that verify them, indexes, triggers, database functions, queues and scheduled jobs (pgcron, pgmq), vector/semantic search (pgvector), and restoring dumps (pgrestore) or importing data. Also load it when diagnosing slow queries, high CPU, timeouts, EXPLAIN plans, connection exhaustion, locking, bloat, or rows visible to the wrong user or tenant. This is not just a performance guide — schema, migration, security, and SQL authoring tasks need these rules too, even for a one-column change or a single query.

OfficialData & Databases462.2K installs

prisma-client-api

prisma/skills

Prisma Client API reference covering model queries, filters, operators, and client methods. Use when writing database queries, using CRUD operations, filtering data, or configuring Prisma Client. Triggers on "prisma query", "findMany", "create", "update", "delete", "$transaction".

OfficialData & Databases393.5K installs

Deprecated compatibility name for prisma-orm-setup. Use only when a user or existing integration explicitly invokes prisma-database-setup; otherwise select prisma-orm-setup.

OfficialData & Databases392.3K installs

prisma-cli

prisma/skills

Prisma ORM 7 CLI reference for existing Prisma 7 applications or explicitly selected Prisma 7 workflows. Covers prisma init, generate, migrate, db, dev, studio, and mcp. For setup, connection repair, or Prisma 8, use prisma-orm-setup; not the Prisma Platform CLI.

OfficialData & Databases390.8K installs

prisma-postgres

prisma/skills

Deprecated compatibility name for prisma-postgres-setup. Use only when a user or existing integration explicitly invokes prisma-postgres; otherwise select prisma-postgres-setup.

OfficialData & Databases384.4K installs

Required reference for Prisma ORM 7 SQL driver adapter work. Use when implementing or modifying adapters, adding database drivers, or touching SqlDriverAdapter, Transaction, savepoint, result mapping, or DriverAdapterError behavior. Covers current transaction lifecycle, optional savepoint hooks, original database-error preservation, and verification.

OfficialData & Databases378.9K installs