ResearchCave Technology

ResearchCave.RateLimiting (1.1.45)

Published 2026-07-20 00:33:22 +03:00 by admin

Installation

dotnet nuget add source --name ResearchCave --username your_username --password your_token http://git.researchcave.com/api/packages/ResearchCave/nuget/index.json
dotnet add package --source ResearchCave --version 1.1.45 ResearchCave.RateLimiting

About this package

Standardized multi-window ASP.NET Core rate limiting, telemetry, and abuse-event aggregation for ResearchCave services.

ResearchCave.RateLimiting

ResearchCave.RateLimiting is the standard ASP.NET Core rate-limiting package for ResearchCave services. It targets .NET 10 and composes the native Microsoft.AspNetCore.RateLimiting and System.Threading.RateLimiting implementations rather than duplicating their algorithms.

Registration

using ResearchCave.RateLimiting;

builder.Services.AddResearchCaveRateLimiting(
    builder.Configuration.GetSection("RateLimiting"));

var app = builder.Build();

// Put forwarded-header middleware before this only when the application's trusted
// proxy/network configuration is explicit. The package itself never trusts X-Forwarded-For.
app.UseRateLimiter();

app.MapControllers()
    .RequireResearchCaveRateLimit("api-general");

The extension normalizes policy names case-insensitively. The standard ASP.NET Core [EnableRateLimiting("api-general")] attribute is also supported; use the normalized lower-case configured name in attributes.

Configuration

{
  "RateLimiting": {
    "Enabled": true,
    "IncludeRetryAfterHeader": true,
    "ServiceName": "Sentient.Server",
    "UserClaimName": "sub",
    "ClientClaimNames": [ "client_id", "azp" ],
    "DefaultSlidingWindowSegments": 6,
    "DefaultQueueLimit": 0,
    "AbuseQueueCapacity": 1024,
    "AbuseStateCapacity": 100000,
    "AbuseStateIdleExpiration": "01:00:00",
    "AbusePublisherMaxAttempts": 2,
    "AbusePublisherRetryDelay": "00:00:01",
    "ShutdownDrainTimeout": "00:00:10",
    "Policies": {
      "api-general": {
        "PartitionBy": [ "User", "Client", "Ip" ],
        "Limits": [
          { "Window": "00:01:00", "PermitLimit": 100 },
          { "Window": "00:15:00", "PermitLimit": 1000 },
          { "Window": "01:00:00", "PermitLimit": 3000 },
          { "Window": "1.00:00:00", "PermitLimit": 20000 }
        ],
        "AbuseReporting": {
          "Enabled": true,
          "ReportAfterRejections": 10,
          "Cooldown": "00:05:00"
        }
      },
      "expensive-operation": {
        "PartitionBy": [ "User", "Client" ],
        "Limits": [
          { "Window": "00:01:00", "PermitLimit": 10, "SegmentsPerWindow": 6 },
          { "Window": "01:00:00", "PermitLimit": 100 },
          { "Window": "30.00:00:00", "PermitLimit": 1000 }
        ]
      }
    }
  }
}

All configured limits in a policy apply simultaneously. Sliding windows are used by default. One minute is recommended for bursts, 15 minutes for sustained automation, one hour for repeated abuse, and 24 hours for daily protection. Use 30 days only for a quota that needs it; it is exactly 30 days and is not a calendar month. Other durations are accepted only when explicitly configured.

QueueLimit defaults to zero so rejected HTTP requests do not occupy server resources while waiting. Segment counts default to six and are startup-validated, including a minimum practical segment duration.

Partitioning and trusted IPs

User reads the authenticated sub claim. Client checks the configured client claims in order. Ip uses HttpContext.Connection.RemoteIpAddress. Anonymous traffic always receives an IP component, even when a policy contains only authenticated dimensions. The internal composite key uses length-prefix encoding followed by SHA-256, so values cannot collide through delimiters and raw identifiers are not exposed to clients or metrics.

Replace IRateLimitIpAddressResolver before registration to integrate ResearchCave's trusted real-IP resolver:

builder.Services.AddSingleton<IRateLimitIpAddressResolver, ResearchCaveRealIpResolver>();
builder.Services.AddResearchCaveRateLimiting(section);

The default resolver deliberately ignores forwarded headers. Configure ASP.NET Core ForwardedHeadersMiddleware with known proxies/networks, or provide a resolver backed by an already-validated real-IP facility. Never trust arbitrary forwarded headers from the public internet.

Rejections, telemetry, and security logs

A rejection is 429 application/problem+json with error type https://errors.researchcave.com/rate-limit-exceeded, code rate_limit_exceeded, and the normalized policy name. The response never contains an internal partition key. Retry-After uses native lease metadata when present; otherwise the exceeded sliding window is returned as a conservative safe delay.

The meter name is ResearchCave.RateLimiting and exposes:

  • researchcave.ratelimit.requests
  • researchcave.ratelimit.rejections
  • researchcave.ratelimit.abuse_events
  • researchcave.ratelimit.abuse_queue_dropped
  • researchcave.ratelimit.abuse_state_evictions

Metric tags are restricted to policy, service, result, and a low-cardinality window category. User IDs, clients, addresses, paths, and partition keys are never metric tags.

Warning logs for rejections contain the policy, service, safe identity dimensions, normalized route pattern, method, exceeded window, permit limit, aggregate rejection count, trace ID, and timestamp. The package never reads or logs authorization headers, cookies, request bodies, passwords, or tokens. OpenTelemetry is evidence for investigation; it does not block users, score risk, or manage incidents.

Abuse-event transport

The default IRateLimitAbuseEventPublisher is a no-op. Replace it with an application transport adapter. The following illustrates a Wolverine publisher; keep the contract transport-neutral and configure Wolverine/NATS in the consuming service:

using ResearchCave.RateLimiting;
using Wolverine;

public sealed class WolverineRateLimitAbuseEventPublisher(IMessageBus bus)
    : IRateLimitAbuseEventPublisher
{
    public async ValueTask PublishAsync(
        RateLimitExceededEvent message,
        CancellationToken cancellationToken)
    {
        await bus.PublishAsync(message);
    }
}

builder.Services.AddSingleton<IRateLimitAbuseEventPublisher,
    WolverineRateLimitAbuseEventPublisher>();

Rejections are atomically aggregated per service + policy + partition + exceeded window by IRateLimitAbuseStateStore. Only a threshold crossing is enqueued, and cooldown suppresses duplicate incidents. The default store has a hard capacity and O(1) least-recently-used eviction, so unique incident keys cannot grow memory without bound. Inactive entries expire after AbuseStateIdleExpiration, but never earlier than their active cooldown. Capacity and expiration evictions are observable through researchcave.ratelimit.abuse_state_evictions.

Capacity eviction deliberately favors bounded memory. If AbuseStateCapacity is undersized for the active incident population, an evicted incident can be recreated during its former cooldown and produce another event. Size it for the service's expected rejected-partition cardinality or replace the store with atomic distributed state.

Publishing occurs through a bounded Channel<T> hosted service, never directly in the HTTP rejection path. Queue overflow drops the new non-durable event and emits a warning and counter. Attempts and retry delay are bounded; shutdown attempts a bounded graceful drain. If the drain times out, the active publisher is cancelled and the number of abandoned in-flight and queued events is logged and added to the dropped-event metric. Publisher exceptions are logged and never change the 429 response.

The default queue is not durable. A service that requires guaranteed delivery should replace the publisher with an outbox-backed implementation that returns after durable handoff.

Distributed-system limitations

The native limiters and default in-memory abuse state are per application instance. With multiple servers:

  • each instance can independently permit traffic;
  • each instance can independently publish an abuse event;
  • cluster-wide enforcement requires a distributed rate-limiting implementation;
  • cluster-wide event deduplication requires an atomic distributed IRateLimitAbuseStateStore implementation.

IDistributedCache.GetAsync followed by SetAsync is not sufficient for deduplication because it is not an atomic acquire operation. Use a Redis script/transaction, database conditional update, or equivalent atomic primitive.

The package intentionally does not implement permanent blocking, banning, IP reputation, incident persistence, admin UI, or email/Telegram/SignalR/push notifications. Those responsibilities remain with the Abuse and Notifications services.

Composite permit behavior

Windows are acquired from shortest to longest and acquisition stops at the first rejection. Native sliding-window leases do not restore permits when disposed. Consequently, if a later window rejects, earlier windows have conservatively consumed a permit. This prevents a caller that is already over a long quota from preserving burst capacity for a boundary, but can make the earlier constraint stricter during sustained rejection. The behavior is covered by a dedicated test.

Benchmarks

ResearchCave.RateLimiting.Benchmarks contains BenchmarkDotNet cases for accepted requests, rejected requests, and 100,000 active partitions:

dotnet run -c Release --project ResearchCave.RateLimiting.Benchmarks
Details
NuGet
2026-07-20 00:33:22 +03:00
1
ResearchCave
52 KiB
Assets (2)
Versions (7) View all
1.1.48 2026-07-28
1.1.47 2026-07-28
1.1.46 2026-07-23
1.1.45 2026-07-20
1.1.44 2026-07-20