mcpField
D
dotnet-rate-limiting

dotnet-rate-limiting

github.com/ronnythedevセキュリティ7760 回インストール15 週間前に更新
リポジトリ

dotnet-rate-limiting について

ASP.NET Core開発者向けのレート制限設定スキルです。ASP.NET Core 7+の組み込みミドルウェアを使い、複数のアルゴリズムやパーティション別ポリシー、エンドポイントへの適用、Redisによる分散制限の例を扱います。

主な機能

  1. 01固定ウィンドウ、スライディングウィンドウ、トークンバケット、同時実行数制限
  2. 02ユーザー単位のポリシー分割
  3. 03エンドポイントとコントローラーへの適用
  4. 04Redis分散レート制限の例
  5. 05設定とアルゴリズム選択のガイド

活用シーン

  1. 01匿名または認証済みリクエストの制限
  2. 02ユーザー単位のAPIレート制限
  3. 03ファイルアップロードなどの同時処理の保護

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/ronnythedev/dotnet-clean-architecture-skills --skill dotnet-rate-limiting

インストール後、Claude Code で/dotnet-rate-limitingを入力して読み込まれたか確認します。

詳細

ASP.NET Core のレート制限ミドルウェア

概要

組み込みのレート制限ミドルウェア(ASP.NET Core 7 以降)は、API を不正利用から保護します。

  • 固定ウィンドウ - 一定の間隔でカウンターをリセットします
  • スライディングウィンドウ - 固定ウィンドウを平滑化します
  • トークンバケット - バーストを許容し、時間の経過とともに補充します
  • 同時実行数 - 同時リクエスト数を制限します

クイックリファレンス

アルゴリズム最適な用途特徴
固定ウィンドウシンプルなレート制限間隔の境界でカウンターをリセットします
スライディングウィンドウより滑らかな制限セグメント間の加重平均を使用します
トークンバケットバーストへの対応バケットサイズまでのバーストを許容します
同時実行数リソース保護並行リクエスト数を制限します

レート制限の構成

/API/RateLimiting/
├── RateLimitingConfiguration.cs
├── Policies/
│   ├── UserRateLimitPolicy.cs
│   └── IpRateLimitPolicy.cs
└── Middleware/
    └── RateLimitExceededHandler.cs

テンプレート:基本的なレート制限の構成

// src/{name}.api/Program.cs
using System.Threading.RateLimiting;
using Microsoft.AspNetCore.RateLimiting;

var builder = WebApplication.CreateBuilder(args);

// ═══════════════════════════════════════════════════════════════
// RATE LIMITING CONFIGURATION
// ═══════════════════════════════════════════════════════════════
builder.Services.AddRateLimiter(options =>
{
    // Global limiter settings
    options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;

    // ═══════════════════════════════════════════════════════════════
    // FIXED WINDOW POLICY
    // Allows N requests per time window, resets at window boundary
    // ═══════════════════════════════════════════════════════════════
    options.AddFixedWindowLimiter("fixed", limiterOptions =>
    {
        limiterOptions.PermitLimit = 100;                           // Requests per window
        limiterOptions.Window = TimeSpan.FromMinutes(1);            // Window duration
        limiterOptions.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
        limiterOptions.QueueLimit = 10;                             // Queue when limit exceeded
    });

    // ═══════════════════════════════════════════════════════════════
    // SLIDING WINDOW POLICY
    // Smoother rate limiting with segments
    // ═══════════════════════════════════════════════════════════════
    options.AddSlidingWindowLimiter("sliding", limiterOptions =>
    {
        limiterOptions.PermitLimit = 100;
        limiterOptions.Window = TimeSpan.FromMinutes(1);
        limiterOptions.SegmentsPerWindow = 6;                       // 6 segments = 10s each
        limiterOptions.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
        limiterOptions.QueueLimit = 10;
    });

    // ═══════════════════════════════════════════════════════════════
    // TOKEN BUCKET POLICY
    // Allows bursts, then rate-limited
    // ═══════════════════════════════════════════════════════════════
    options.AddTokenBucketLimiter("token", limiterOptions =>
    {
        limiterOptions.TokenLimit = 100;                            // Maximum tokens (bucket size)
        limiterOptions.ReplenishmentPeriod = TimeSpan.FromSeconds(1);
        limiterOptions.TokensPerPeriod = 10;                        // Tokens added per period
        limiterOptions.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
        limiterOptions.QueueLimit = 10;
        limiterOptions.AutoReplenishment = true;
    });

    // ═══════════════════════════════════════════════════════════════
    // CONCURRENCY LIMITER
    // Limits simultaneous requests (not rate)
    // ═══════════════════════════════════════════════════════════════
    options.AddConcurrencyLimiter("concurrency", limiterOptions =>
    {
        limiterOptions.PermitLimit = 50;                            // Max concurrent requests
        limiterOptions.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
        limiterOptions.QueueLimit = 25;
    });

    // Custom response for rejected requests
    options.OnRejected = async (context, cancellationToken) =>
    {
        if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
        {
            context.HttpContext.Response.Headers.RetryAfter =
                ((int)retryAfter.TotalSeconds).ToString();
        }

        context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
        await context.HttpContext.Response.WriteAsJsonAsync(new
        {
            error = "Too many requests",
            message = "Rate limit exceeded. Please try again later.",
            retryAfterSeconds = retryAfter?.TotalSeconds ?? 60
        }, cancellationToken);
    };
});

var app = builder.Build();

// ═══════════════════════════════════════════════════════════════
// MIDDLEWARE ORDER - Rate limiting before authentication
// ═══════════════════════════════════════════════════════════════
app.UseRateLimiter();
app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();

テンプレート:ポリシーベースのレート制限

// src/{name}.api/RateLimiting/RateLimitPolicies.cs
namespace {name}.api.ratelimiting;

public static class RateLimitPolicies
{
    // Policy names
    public const string Anonymous = "anonymous";
    public const string Authenticated = "authenticated";
    public const string Premium = "premium";
    public const string Api = "api";
    public const string Upload = "upload";
}
// src/{name}.api/RateLimiting/RateLimitingConfiguration.cs
using System.Threading.RateLimiting;
using Microsoft.AspNetCore.RateLimiting;

namespace {name}.api.ratelimiting;

public static class RateLimitingConfiguration
{
    public static IServiceCollection AddRateLimitingPolicies(
        this IServiceCollection services,
        IConfiguration configuration)
    {
        var settings = configuration
            .GetSection("RateLimiting")
            .Get<RateLimitSettings>() ?? new RateLimitSettings();

        services.AddRateLimiter(options =>
        {
            options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;

            // ═══════════════════════════════════════════════════════════════
            // ANONYMOUS USERS - Stricter limits
            // ═══════════════════════════════════════════════════════════════
            options.AddFixedWindowLimiter(RateLimitPolicies.Anonymous, limiterOptions =>
            {
                limiterOptions.PermitLimit = settings.AnonymousRequestsPerMinute;
                limiterOptions.Window = TimeSpan.FromMinutes(1);
                limiterOptions.QueueLimit = 5;
            });

            // ═══════════════════════════════════════════════════════════════
            // AUTHENTICATED USERS - More generous limits
            // ═══════════════════════════════════════════════════════════════
            options.AddTokenBucketLimiter(RateLimitPolicies.Authenticated, limiterOptions =>
            {
                limiterOptions.TokenLimit = settings.AuthenticatedBurstLimit;
                limiterOptions.ReplenishmentPeriod = TimeSpan.FromSeconds(1);
                limiterOptions.TokensPerPeriod = settings.AuthenticatedTokensPerSecond;
                limiterOptions.QueueLimit = 20;
            });

            // ═══════════════════════════════════════════════════════════════
            // PREMIUM USERS - Highest limits
            // ═══════════════════════════════════════════════════════════════
            options.AddTokenBucketLimiter(RateLimitPolicies.Premium, limiterOptions =>
            {
                limiterOptions.TokenLimit = settings.PremiumBurstLimit;
                limiterOptions.ReplenishmentPeriod = TimeSpan.FromSeconds(1);
                limiterOptions.TokensPerPeriod = settings.PremiumTokensPerSecond;
                limiterOptions.QueueLimit = 50;
            });

            // ═══════════════════════════════════════════════════════════════
            // API ENDPOINTS - Per-endpoint limiting
            // ═══════════════════════════════════════════════════════════════
            options.AddSlidingWindowLimiter(RateLimitPolicies.Api, limiterOptions =>
            {
                limiterOptions.PermitLimit = settings.ApiRequestsPerMinute;
                limiterOptions.Window = TimeSpan.FromMinutes(1);
                limiterOptions.SegmentsPerWindow = 6;
                limiterOptions.QueueLimit = 10;
            });

            // ═══════════════════════════════════════════════════════════════
            // FILE UPLOADS - Concurrency limited
            // ═══════════════════════════════════════════════════════════════
            options.AddConcurrencyLimiter(RateLimitPolicies.Upload, limiterOptions =>
            {
                limiterOptions.PermitLimit = settings.MaxConcurrentUploads;
                limiterOptions.QueueLimit = settings.UploadQueueSize;
            });

            // Global rejection handler
            options.OnRejected = HandleRejection;
        });

        return services;
    }

    private static async ValueTask HandleRejection(
        OnRejectedContext context,
        CancellationToken cancellationToken)
    {
        var response = context.HttpContext.Response;

        // Set retry-after header if available
        if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
        {
            response.Headers.RetryAfter = ((int)retryAfter.TotalSeconds).ToString();
        }

        // Log the rejection
        var logger = context.HttpContext.RequestServices
            .GetRequiredService<ILogger<RateLimitingConfiguration>>();

        var clientIp = context.HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown";
        var path = context.HttpContext.Request.Path;

        logger.LogWarning(
            "Rate limit exceeded for client {ClientIp} on path {Path}",
            clientIp,
            path);

        response.StatusCode = StatusCodes.Status429TooManyRequests;
        response.ContentType = "application/json";

        await response.WriteAsJsonAsync(new
        {
            type = "https://tools.ietf.org/html/rfc6585#section-4",
            title = "Too Many Requests",
            status = 429,
            detail = "Rate limit exceeded. Please slow down your requests.",
            retryAfter = retryAfter?.TotalSeconds ?? 60
        }, cancellationToken);
    }
}
設定クラス
// src/{name}.api/RateLimiting/RateLimitSettings.cs
namespace {name}.api.ratelimiting;

public sealed class RateLimitSettings
{
    public int AnonymousRequestsPerMinute { get; set; } = 30;
    public int AuthenticatedBurstLimit { get; set; } = 100;
    public int AuthenticatedTokensPerSecond { get; set; } = 10;
    public int PremiumBurstLimit { get; set; } = 500;
    public int PremiumTokensPerSecond { get; set; } = 50;
    public int ApiRequestsPerMinute { get; set; } = 60;
    public int MaxConcurrentUploads { get; set; } = 5;
    public int UploadQueueSize { get; set; } = 10;
}
appsettings.json
{
  "RateLimiting": {
    "AnonymousRequestsPerMinute": 30,
    "AuthenticatedBurstLimit": 100,
    "AuthenticatedTokensPerSecond": 10,
    "PremiumBurstLimit": 500,
    "PremiumTokensPerSecond": 50,
    "ApiRequestsPerMinute": 60,
    "MaxConcurrentUploads": 5,
    "UploadQueueSize": 10
  }
}

テンプレート:ユーザーベースのレート制限(パーティション分割)

// src/{name}.api/Program.cs
builder.Services.AddRateLimiter(options =>
{
    // ═══════════════════════════════════════════════════════════════
    // PARTITIONED BY USER ID
    // Each user has their own rate limit bucket
    // ═══════════════════════════════════════════════════════════════
    options.AddPolicy("per-user", httpContext =>
    {
        var userId = httpContext.User.FindFirstValue(ClaimTypes.NameIdentifier);

        if (string.IsNullOrEmpty(userId))
        {
            // Anonymous users - stricter limit, partition by IP
            var clientIp = httpContext.Connection.RemoteIpAddress?.ToString() ?? "anonymous";

            return RateLimitPartition.GetFixedWindowLimiter(
                partitionKey: clientIp,
                factory: _ => new FixedWindowRateLimiterOptions
                {
                    PermitLimit = 30,
                    Window = TimeSpan.FromMinutes(1)
                });
        }

        // Authenticated users - more generous
        return RateLimitPartition.GetTokenBucketLimiter(
            partitionKey: userId,
            factory: _ => new TokenBucketRateLimiterOptions
            {
                TokenLimit = 100,
                ReplenishmentPeriod = TimeSpan.FromSeconds(1),
                TokensPerPeriod = 10
            });
    });

    // ═══════════════════════════════════════════════════════════════
    // PARTITIONED BY SUBSCRIPTION TIER
    // Different limits based on user's subscription
    // ═══════════════════════════════════════════════════════════════
    options.AddPolicy("tier-based", httpContext =>
    {
        var userId = httpContext.User.FindFirstValue(ClaimTypes.NameIdentifier) ?? "anonymous";
        var tier = httpContext.User.FindFirstValue("subscription_tier") ?? "free";

        return tier switch
        {
            "enterprise" => RateLimitPartition.GetNoLimiter(userId),

            "premium" => RateLimitPartition.GetTokenBucketLimiter(
                partitionKey: userId,
                factory: _ => new TokenBucketRateLimiterOptions
                {
                    TokenLimit = 500,
                    ReplenishmentPeriod = TimeSpan.FromSeconds(1),
                    TokensPerPeriod = 50
                }),

            "basic" => RateLimitPartition.GetTokenBucketLimiter(
                partitionKey: userId,
                factory: _ => new TokenBucketRateLimiterOptions
                {
                    TokenLimit = 100,
                    ReplenishmentPeriod = TimeSpan.FromSeconds(1),
                    TokensPerPeriod = 10
                }),

            _ => RateLimitPartition.GetFixedWindowLimiter(
                partitionKey: userId,
                factory: _ => new FixedWindowRateLimiterOptions
                {
                    PermitLimit = 20,
                    Window = TimeSpan.FromMinutes(1)
                })
        };
    });
});

テンプレート:エンドポイントへのレート制限の適用

Minimal API エンドポイント
// src/{name}.api/Endpoints/OrderEndpoints.cs
public static class OrderEndpoints
{
    public static void MapOrderEndpoints(this IEndpointRouteBuilder app)
    {
        var group = app.MapGroup("/api/orders")
            .WithTags("Orders")
            .RequireAuthorization();

        // ═══════════════════════════════════════════════════════════════
        // APPLY RATE LIMIT TO SINGLE ENDPOINT
        // ═══════════════════════════════════════════════════════════════
        group.MapGet("/", GetOrders)
            .RequireRateLimiting(RateLimitPolicies.Api);

        group.MapGet("/{id:guid}", GetOrderById)
            .RequireRateLimiting(RateLimitPolicies.Api);

        // ═══════════════════════════════════════════════════════════════
        // STRICTER LIMIT FOR WRITE OPERATIONS
        // ═══════════════════════════════════════════════════════════════
        group.MapPost("/", CreateOrder)
            .RequireRateLimiting(RateLimitPolicies.Authenticated);

        // ═══════════════════════════════════════════════════════════════
        // DISABLE RATE LIMITING FOR SPECIFIC ENDPOINT
        // ═══════════════════════════════════════════════════════════════
        group.MapGet("/health", () => Results.Ok())
            .DisableRateLimiting();
    }
}
コントローラー
// src/{name}.api/Controllers/OrdersController.cs
using Microsoft.AspNetCore.RateLimiting;

namespace {name}.api.controllers;

[ApiController]
[Route("api/[controller]")]
[EnableRateLimiting(RateLimitPolicies.Api)]  // Apply to all actions
public sealed class OrdersController : ControllerBase
{
    [HttpGet]
    public async Task<IActionResult> GetOrders()
    {
        // Uses controller-level rate limit
    }

    [HttpGet("{id:guid}")]
    public async Task<IActionResult> GetOrderById(Guid id)
    {
        // Uses controller-level rate limit
    }

    [HttpPost]
    [EnableRateLimiting(RateLimitPolicies.Authenticated)]  // Override for this action
    public async Task<IActionResult> CreateOrder([FromBody] CreateOrderRequest request)
    {
        // Uses action-level rate limit
    }

    [HttpGet("export")]
    [EnableRateLimiting(RateLimitPolicies.Premium)]  // Expensive operation
    public async Task<IActionResult> ExportOrders()
    {
        // Uses premium rate limit
    }

    [HttpGet("health")]
    [DisableRateLimiting]  // Health checks shouldn't be rate limited
    public IActionResult Health()
    {
        return Ok();
    }
}
グループ単位のレート制限
// src/{name}.api/Endpoints/PublicApiEndpoints.cs
public static void MapPublicApiEndpoints(this IEndpointRouteBuilder app)
{
    // Apply rate limit to entire group
    var group = app.MapGroup("/api/public")
        .RequireRateLimiting(RateLimitPolicies.Anonymous);

    group.MapGet("/products", GetProducts);
    group.MapGet("/categories", GetCategories);
}

テンプレート:Redis を使用した分散レート制限

// src/{name}.api/RateLimiting/RedisRateLimitingConfiguration.cs
using System.Threading.RateLimiting;
using StackExchange.Redis;

namespace {name}.api.ratelimiting;

/// <summary>
/// For distributed systems, implement custom rate limiter with Redis
/// to share rate limit state across multiple instances.
/// </summary>
public static class RedisRateLimitingConfiguration
{
    public static IServiceCollection AddDistributedRateLimiting(
        this IServiceCollection services,
        IConfiguration configuration)
    {
        var redisConnection = configuration.GetConnectionString("Redis");

        services.AddSingleton<IConnectionMultiplexer>(
            ConnectionMultiplexer.Connect(redisConnection!));

        services.AddRateLimiter(options =>
        {
            options.AddPolicy("distributed", context =>
            {
                var redis = context.RequestServices
                    .GetRequiredService<IConnectionMultiplexer>();

                var userId = context.User.FindFirstValue(ClaimTypes.NameIdentifier)
                    ?? context.Connection.RemoteIpAddress?.ToString()
                    ?? "anonymous";

                return RateLimitPartition.Get(
                    partitionKey: userId,
                    factory: key => new RedisRateLimiter(
                        redis: redis,
                        partitionKey: key,
                        options: new RedisRateLimiterOptions
                        {
                            PermitLimit = 100,
                            Window = TimeSpan.FromMinutes(1)
                        }));
            });
        });

        return services;
    }
}

/// <summary>
/// Custom rate limiter using Redis for distributed state
/// </summary>
public sealed class RedisRateLimiter : RateLimiter
{
    private readonly IConnectionMultiplexer _redis;
    private readonly string _partitionKey;
    private readonly RedisRateLimiterOptions _options;

    public RedisRateLimiter(
        IConnectionMultiplexer redis,
        string partitionKey,
        RedisRateLimiterOptions options)
    {
        _redis = redis;
        _partitionKey = partitionKey;
        _options = options;
    }

    public override TimeSpan? IdleDuration => null;

    public override RateLimiterStatistics? GetStatistics() => null;

    protected override async ValueTask<RateLimitLease> AcquireAsyncCore(
        int permitCount,
        CancellationToken cancellationToken)
    {
        var db = _redis.GetDatabase();
        var key = $"rate_limit:{_partitionKey}";
        var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
        var windowStart = now - (long)_options.Window.TotalSeconds;

        // Remove old entries outside the window
        await db.SortedSetRemoveRangeByScoreAsync(key, 0, windowStart);

        // Count current requests in window
        var currentCount = await db.SortedSetLengthAsync(key);

        if (currentCount >= _options.PermitLimit)
        {
            // Get oldest entry to calculate retry-after
            var oldest = await db.SortedSetRangeByRankWithScoresAsync(key, 0, 0);
            var retryAfter = oldest.Length > 0
                ? TimeSpan.FromSeconds(oldest[0].Score + _options.Window.TotalSeconds - now)
                : _options.Window;

            return new RedisRateLimitLease(false, retryAfter);
        }

        // Add new request
        await db.SortedSetAddAsync(key, Guid.NewGuid().ToString(), now);
        await db.KeyExpireAsync(key, _options.Window.Add(TimeSpan.FromMinutes(1)));

        return new RedisRateLimitLease(true, null);
    }

    protected override RateLimitLease AttemptAcquireCore(int permitCount)
    {
        // Synchronous not supported for Redis
        return new RedisRateLimitLease(false, _options.Window);
    }
}

public sealed class RedisRateLimitLease : RateLimitLease
{
    private readonly TimeSpan? _retryAfter;

    public RedisRateLimitLease(bool isAcquired, TimeSpan? retryAfter)
    {
        IsAcquired = isAcquired;
        _retryAfter = retryAfter;
    }

    public override bool IsAcquired { get; }

    public override IEnumerable<string> MetadataNames =>
        _retryAfter.HasValue ? new[] { MetadataName.RetryAfter.Name } : Array.Empty<string>();

    public override bool TryGetMetadata(string metadataName, out object? metadata)
    {
        if (metadataName == MetadataName.RetryAfter.Name && _retryAfter.HasValue)
        {
            metadata = _retryAfter.Value;
            return true;
        }

        metadata = null;
        return false;
    }
}

public sealed class RedisRateLimiterOptions
{
    public int PermitLimit { get; set; } = 100;
    public TimeSpan Window { get; set; } = TimeSpan.FromMinutes(1);
}

アルゴリズム選択ガイド

┌────────────────────────────────────────────────────────────────┐
│                Which Rate Limiting Algorithm?                   │
├────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Need to limit concurrent requests (not rate)?                  │
│       │                                                         │
│       └── YES ──► Use Concurrency Limiter                       │
│                   (file uploads, heavy operations)              │
│                                                                 │
│  Need to allow bursts?                                          │
│       │                                                         │
│       └── YES ──► Use Token Bucket                              │
│                   (API calls, user actions)                     │
│                                                                 │
│  Need smooth rate limiting?                                     │
│       │                                                         │
│       └── YES ──► Use Sliding Window                            │
│                   (API rate limits, quota enforcement)          │
│                                                                 │
│  Simple time-based limit?                                       │
│       │                                                         │
│       └── YES ──► Use Fixed Window                              │
│                   (login attempts, simple limits)               │
│                                                                 │
└────────────────────────────────────────────────────────────────┘

重要なルール

  1. 適切なアルゴリズムを選択する - バーストにはトークンバケット、滑らかな制限にはスライディングウィンドウを使用します
  2. ユーザーごとにパーティション分割する - IP アドレスのみでのパーティション分割は避けます(NAT に関する問題があるため)
  3. キューの上限を設定する - メモリの枯渇を防ぎます
  4. Retry-After ヘッダーを含める - クライアントが適切にリクエストを控えられるようにします
  5. 拒否をログに記録する - 不正利用のパターンを監視します
  6. 負荷をかけてテストする - 想定どおりに制限が機能することを確認します
  7. 分散状態を使用する - 複数インスタンスのデプロイでは Redis を使用します
  8. ヘルスチェックを除外する - 監視エンドポイントにはレート制限を適用しません
  9. ティアごとに異なる制限を設定する - プレミアムユーザーにはより高い制限を設定します
  10. ミドルウェアの順序が重要です - 匿名ユーザー向けの制限では、認証の前に配置します

避けるべきアンチパターン

// ❌ WRONG: Partition only by IP (NAT can share IPs)
options.AddPolicy("bad", context =>
    RateLimitPartition.GetFixedWindowLimiter(
        context.Connection.RemoteIpAddress?.ToString() ?? "unknown",
        _ => new FixedWindowRateLimiterOptions { /* ... */ }));

// ✅ CORRECT: Prefer user ID, fallback to IP
options.AddPolicy("good", context =>
{
    var userId = context.User.FindFirstValue(ClaimTypes.NameIdentifier);
    var partitionKey = userId ?? context.Connection.RemoteIpAddress?.ToString() ?? "anonymous";
    return RateLimitPartition.GetFixedWindowLimiter(partitionKey, /* ... */);
});

// ❌ WRONG: No queue limit (memory exhaustion risk)
options.AddFixedWindowLimiter("bad", limiter =>
{
    limiter.PermitLimit = 100;
    limiter.Window = TimeSpan.FromMinutes(1);
    // Missing QueueLimit!
});

// ✅ CORRECT: Always set queue limit
options.AddFixedWindowLimiter("good", limiter =>
{
    limiter.PermitLimit = 100;
    limiter.Window = TimeSpan.FromMinutes(1);
    limiter.QueueLimit = 10;  // Bounded queue
});

// ❌ WRONG: Rate limiting health endpoints
app.MapGet("/health", () => Results.Ok())
    .RequireRateLimiting("api");

// ✅ CORRECT: Exempt health checks
app.MapGet("/health", () => Results.Ok())
    .DisableRateLimiting();

// ❌ WRONG: Same limits for all users
options.AddFixedWindowLimiter("api", limiter =>
{
    limiter.PermitLimit = 100;  // Same for everyone
});

// ✅ CORRECT: Tier-based limits
options.AddPolicy("api", context =>
{
    var tier = context.User.FindFirstValue("tier") ?? "free";
    var limit = tier switch { "premium" => 500, "basic" => 100, _ => 20 };
    return RateLimitPartition.GetFixedWindowLimiter(/* ... */);
});

関連スキル

  • 07-minimal-api-endpoints - エンドポイントにレート制限を適用します
  • 12-dotnet-jwt-authentication - パーティション分割のためのユーザー識別
  • 24-logging-configuration - レート制限イベントをログに記録します
  • 17-dotnet-health-checks - レート制限から除外します

コメント