Skip to content

Rate Limiting

Granit.RateLimiting provides per-tenant rate limiting with four algorithms, configurable policies, Redis-backed counters (with in-memory fallback), and Wolverine message handler support. Integrates with Granit.Features for plan-based dynamic quotas.

[DependsOn(typeof(GranitRateLimitingModule))]
public class AppModule : GranitModule { }
{
"RateLimiting": {
"Enabled": true,
"KeyPrefix": "rl",
"BypassRoles": ["admin"],
"FallbackOnCounterStoreFailure": "Allow",
"Policies": {
"api-default": {
"Algorithm": "SlidingWindow",
"PermitLimit": 1000,
"Window": "00:01:00",
"SegmentsPerWindow": 6
},
"api-sensitive": {
"Algorithm": "TokenBucket",
"TokenLimit": 50,
"TokensPerPeriod": 10,
"ReplenishmentPeriod": "00:00:10"
}
}
}
}
app.MapGet("/api/v1/appointments", GetAppointments)
.RequireGranitRateLimiting("api-default");
app.MapPost("/api/v1/payments", ProcessPayment)
.RequireGranitRateLimiting("api-sensitive");

Decorate message types with [RateLimited] and register the Wolverine middleware:

[RateLimited("api-default")]
public record SyncPatientCommand(Guid PatientId);
// In Wolverine configuration
opts.Policies.AddMiddleware<RateLimitMiddleware>(
chain => chain.MessageType.GetCustomAttributes(typeof(RateLimitedAttribute), true).Length > 0);

When the rate limit is exceeded, RateLimitExceededException is thrown and handled by Wolverine’s retry policy.

AlgorithmUse caseKey parameters
SlidingWindowGeneral API rate limiting (default)PermitLimit, Window, SegmentsPerWindow
FixedWindowSimple counter, lowest memoryPermitLimit, Window
TokenBucketControlled burst allowanceTokenLimit, TokensPerPeriod, ReplenishmentPeriod
ConcurrencyLimit simultaneous in-flight requestsPermitLimit

Rate limit counters are partitioned by tenant. The Redis key uses hash tags to ensure all keys for a tenant hash to the same Redis Cluster slot:

{prefix}:{{tenantId|global}}:{policyName}

When Redis is unavailable, the FallbackOnCounterStoreFailure setting controls behavior:

ValueBehaviorUse case
AllowLet the request through, log warningPrefer availability over quota enforcement
DenyReject with 429Conservative — prefer safety over availability
{
"status": 429,
"title": "Too Many Requests",
"detail": "Rate limit exceeded for policy 'api-default'. Retry after 10s.",
"policy": "api-default",
"limit": 1000,
"remaining": 0,
"retryAfter": 10
}

The Retry-After header is also set on the response.

When UseFeatureBasedQuotas is enabled, the permit limit is resolved dynamically from Granit.Features (e.g., per-plan quotas). The convention-based feature name is RateLimit.{PolicyName}, overridable via RateLimitPolicyOptions.FeatureName.

PropertyDefaultDescription
EnabledtrueEnable/disable rate limiting globally
KeyPrefix"rl"Redis key prefix
FallbackOnCounterStoreFailureAllowBehavior when Redis is down
BypassRoles[]Roles that skip rate limiting
UseFeatureBasedQuotasfalseUse Granit.Features for dynamic quotas
Policies.*Named rate limiting policies (see below)

Policy options:

PropertyDefaultDescription
AlgorithmSlidingWindowRate limiting algorithm
PermitLimit1000Max permits per window
Window00:01:00Window duration
SegmentsPerWindow6Sliding window segments (accuracy vs. memory)
TokenLimit50Max tokens (TokenBucket only)
TokensPerPeriod10Tokens added per replenishment (TokenBucket only)
ReplenishmentPeriod00:00:10Replenishment interval (TokenBucket only)
FeatureNamenullOverride feature name for dynamic quotas
CategoryKey typesPackage
ModuleGranitRateLimitingModule
StoreIRateLimitCounterStore, IRateLimitQuotaProvider, RateLimitResultGranit.RateLimiting
AttributesRateLimitedAttributeGranit.RateLimiting
ExceptionsRateLimitExceededExceptionGranit.RateLimiting
OptionsGranitRateLimitingOptions, RateLimitPolicyOptionsGranit.RateLimiting
ExtensionsAddGranitRateLimiting(), .RequireGranitRateLimiting()Granit.RateLimiting