.NET Tutorial
Middle5 мин чтенияВопрос 17 из 19

Rate Limiting в ASP.NET Core 7+?

Алгоритмы

Fixed Window: 100 запросов в минуту

10:00–10:01: счётчик 0 → 100

10:01 сброс в 0

Проблема: 100 запросов в 10:00:59
+ 100 в 10:01:00 = 200 за 2 секунды

Sliding Window: окно из сегментов

окно 60 с = 6 сегментов по 10 с

сегменты «уходят» по одному —
всплеск на границе сглажен

Token Bucket: ведро на 20 токенов

каждые 10 с +5 токенов

запрос забирает токен,
нет токенов — отказ или очередь

разрешает всплески до ёмкости ведра

Concurrency: не более 10 одновременно

счётчик активных запросов

освобождается по завершении запроса

Fixed Window: 100 запросов в минуту

10:00–10:01: счётчик 0 → 100

10:01 сброс в 0

Проблема: 100 запросов в 10:00:59
+ 100 в 10:01:00 = 200 за 2 секунды

Sliding Window: окно из сегментов

окно 60 с = 6 сегментов по 10 с

сегменты «уходят» по одному —
всплеск на границе сглажен

Token Bucket: ведро на 20 токенов

каждые 10 с +5 токенов

запрос забирает токен,
нет токенов — отказ или очередь

разрешает всплески до ёмкости ведра

Concurrency: не более 10 одновременно

счётчик активных запросов

освобождается по завершении запроса

Четыре алгоритма ограничения
Алгоритм Что ограничивает Плюсы Минусы Когда
Fixed Window Запросы за интервал Прост, дёшев Двойной всплеск на границе окон Грубые квоты (в день/час)
Sliding Window Запросы за скользящий интервал Сглаживает границы Чуть больше памяти Публичные API
Token Bucket Средняя скорость + всплески Естественно для пользователей: разрешает «пачки» Сложнее объяснить клиентам API с неравномерной нагрузкой
Concurrency Одновременные запросы Защищает ресурс (БД, тяжёлый отчёт) Не ограничивает частоту Дорогие эндпоинты, экспорт, загрузка файлов

Настройка

C#
builder.Services.AddRateLimiter(options =>
{
    options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;

    // 1. Фиксированное окно — для логина (защита от перебора паролей)
    options.AddFixedWindowLimiter("login", o =>
    {
        o.PermitLimit = 5;
        o.Window = TimeSpan.FromMinutes(1);
        o.QueueLimit = 0;                              // без очереди — сразу отказ
    });

    // 2. Ограничение параллельности для тяжёлого отчёта
    options.AddConcurrencyLimiter("reports", o =>
    {
        o.PermitLimit = 3;
        o.QueueLimit = 10;                             // до 10 запросов ждут в очереди
        o.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
    });

    // 3. Token bucket на пользователя (или IP для анонимных)
    options.AddPolicy("per-user", httpContext =>
    {
        var key = httpContext.User.FindFirstValue("sub")
                  ?? httpContext.Connection.RemoteIpAddress?.ToString()
                  ?? "anonymous";
        return RateLimitPartition.GetTokenBucketLimiter(key, _ => new TokenBucketRateLimiterOptions
        {
            TokenLimit = 20,                            // ёмкость ведра (всплеск)
            TokensPerPeriod = 5,
            ReplenishmentPeriod = TimeSpan.FromSeconds(10),
            QueueLimit = 0,
            AutoReplenishment = true
        });
    });

    // Глобальный лимит для всех запросов
    options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(ctx =>
        RateLimitPartition.GetConcurrencyLimiter("global", _ => new ConcurrencyLimiterOptions { PermitLimit = 1000, QueueLimit = 0 }));

    // Ответ при отказе
    options.OnRejected = async (ctx, ct) =>
    {
        if (ctx.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
            ctx.HttpContext.Response.Headers.RetryAfter = ((int)retryAfter.TotalSeconds).ToString();
        await ctx.HttpContext.Response.WriteAsJsonAsync(new ProblemDetails
        {
            Status = 429, Title = "Слишком много запросов", Detail = "Повторите позже"
        }, ct);
    };
});

var app = builder.Build();
app.UseRouting();
app.UseAuthentication();
app.UseRateLimiter();              // после аутентификации, если лимиты зависят от пользователя

app.MapPost("/auth/login", Login).RequireRateLimiting("login");
app.MapGet("/reports/sales", SalesReport).RequireRateLimiting("reports");
app.MapGroup("/api").RequireRateLimiting("per-user");
app.MapGet("/health", () => "ok").DisableRateLimiting();
ЭндпоинтПартиция user:42RateLimitermiddlewareКлиентalt[токены есть][токенов нет и очередь заполнена]GET /api/ordersAcquireAsync(1)lease: разрешеновыполнить200 OKlease: отказ, RetryAfter = 8 с429 Too Many Requests,Retry-After: 8
ЭндпоинтПартиция user:42RateLimitermiddlewareКлиентalt[токены есть][токенов нет и очередь заполнена]GET /api/ordersAcquireAsync(1)lease: разрешеновыполнить200 OKlease: отказ, RetryAfter = 8 с429 Too Many Requests,Retry-After: 8
Путь запроса через rate limiter

Где ограничивать

Интернет

CDN / WAF
DDoS, боты

API Gateway / Ingress
глобальные квоты по ключу,
распределённые лимиты

ASP.NET Core
лимиты по бизнес-логике:
логин, тяжёлые операции

Интернет

CDN / WAF
DDoS, боты

API Gateway / Ingress
глобальные квоты по ключу,
распределённые лимиты

ASP.NET Core
лимиты по бизнес-логике:
логин, тяжёлые операции

Уровни защиты
  • Встроенный лимитер хранит счётчики в памяти процесса. При 5 экземплярах лимит «100 в минуту» фактически станет ~500. Для точных глобальных квот (тарифы API) — Redis (библиотеки вроде RedisRateLimiting), API-шлюз (Kong, YARP с распределённым хранилищем, Azure API Management) или Ingress (nginx.ingress.kubernetes.io/limit-rps).
  • Лимиты в приложении идеально подходят для защиты ресурсов экземпляра (concurrency) и бизнес-сценариев (логин, отправка SMS).

Хорошие практики

  • Партиционируйте по аутентифицированному пользователю/ключу, а для анонимных — по IP (с учётом ForwardedHeaders, иначе все клиенты будут одним IP прокси).
  • Возвращайте Retry-After и документируйте лимиты; часто добавляют заголовки RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset.
  • Разные лимиты для разных тарифов: партиция по тарифу + ключу.
  • Не ограничивайте health checks и внутренние эндпоинты мониторинга.
  • Клиенты должны уважать 429: экспоненциальная задержка с jitter, Polly обрабатывает Retry-After.

Вопросы на засыпку

Чем 429 отличается от 503?

429 Too Many Requests — конкретный клиент превысил свою квоту. 503 Service Unavailable — сервис перегружен или недоступен для всех. Оба могут содержать Retry-After.

Зачем нужна очередь (QueueLimit) в лимитере?

Вместо мгновенного отказа запрос ждёт освобождения разрешения. Это сглаживает кратковременные всплески, но увеличивает задержку и держит соединения. Для интерактивных API очередь обычно маленькая или нулевая.

Как rate limiting связан с backpressure?

Rate limiting — один из механизмов backpressure на границе системы: сервер сообщает клиенту «притормози». Concurrency limiter защищает внутренние ресурсы от перегрузки, превращая её в быстрые отказы вместо деградации всего сервиса.

Что будет с памятью при партиционировании по IP?

Каждая партиция — отдельный лимитер в памяти. Встроенная реализация удаляет неактивные партиции по таймеру, но атака с миллионами IP может раздуть память. Поэтому для анонимного трафика — защита на уровне WAF/шлюза, а в приложении — глобальный лимит.

Итог

Встроенный rate limiter ASP.NET Core предлагает четыре алгоритма и партиционирование по любому ключу. Используйте его для защиты ресурсов экземпляра и чувствительных эндпоинтов, возвращайте 429 с Retry-After, а глобальные квоты в кластере реализуйте на шлюзе или через распределённое хранилище.