Skip to main content

RateLimiterStrategy

The RateLimiterStrategy controls the rate of operations to prevent system overload and ensure fair resource usage.

Overview​

The rate limiter strategy throttles operations to stay within specified limits. It helps prevent system overload, ensures fair resource allocation, and protects against abuse or runaway processes.

class RateLimiterStrategy<T> extends ResilienceStrategy<T> {
RateLimiterStrategy(RateLimiterStrategyOptions<T> options);

Future<Outcome<T>> executeCore<T>(
ResilienceCallback<T> callback,
ResilienceContext context,
ResilienceCallback<T> next,
);
}

class RateLimiterStrategyOptions<T> {
const RateLimiterStrategyOptions({
required this.permitLimit,
this.window = const Duration(seconds: 60),
this.queueLimit = 0,
this.onRateLimitExceeded,
});
}

RateLimiterStrategyOptions Properties​

permitLimit​

Maximum number of permits allowed within the specified window.

Type: int
Required: Yes

RateLimiterStrategyOptions(
permitLimit: 100, // 100 operations per window
)

window​

Time window for the rate limit.

Type: Duration
Default: Duration(seconds: 60)

RateLimiterStrategyOptions(
permitLimit: 100,
window: Duration(minutes: 1), // 100 operations per minute
)

queueLimit​

Maximum number of operations to queue when rate limit is exceeded.

Type: int
Default: 0 (no queueing)

RateLimiterStrategyOptions(
permitLimit: 100,
queueLimit: 10, // Queue up to 10 operations
)

onRateLimitExceeded​

Callback invoked when the rate limit is exceeded.

Type: OnRateLimitExceededCallback<T>?
Default: null

RateLimiterStrategyOptions(
permitLimit: 100,
onRateLimitExceeded: (context, args) {
logger.warn('Rate limit exceeded: ${args.retryAfter}');
},
)

Callback Types​

OnRateLimitExceededCallback<T>​

typedef OnRateLimitExceededCallback<T> = void Function(
ResilienceContext context,
OnRateLimitExceededArgs args,
);

Called when the rate limit is exceeded.

OnRateLimitExceededArgs Properties:

  • retryAfter - Duration to wait before retrying
  • currentCount - Current number of operations in the window

Usage Examples​

Basic Rate Limiting​

final rateLimiter = RateLimiterStrategy(RateLimiterStrategyOptions(
permitLimit: 100,
window: Duration(minutes: 1),
));

final pipeline = ResiliencePipelineBuilder()
.addStrategy(rateLimiter)
.build();

API Rate Limiting​

final apiRateLimiter = RateLimiterStrategy(RateLimiterStrategyOptions(
permitLimit: 1000, // 1000 requests per hour
window: Duration(hours: 1),
onRateLimitExceeded: (context, args) {
final endpoint = context.getProperty<String>('endpoint') ?? 'unknown';
logger.warn('API rate limit exceeded for $endpoint');

// Track rate limit violations
metrics.incrementCounter('rate_limit_exceeded', {
'endpoint': endpoint,
});
},
));

// Usage with context
final context = ResilienceContext();
context.setProperty('endpoint', '/api/users');

final response = await pipeline.execute(
(context) => apiClient.get('/api/users'),
context: context,
);

Database Connection Rate Limiting​

final dbRateLimiter = RateLimiterStrategy(RateLimiterStrategyOptions(
permitLimit: 50, // 50 concurrent operations
window: Duration(seconds: 1),
queueLimit: 20, // Queue up to 20 operations
onRateLimitExceeded: (context, args) {
logger.debug('Database rate limit reached, queueing operation');
},
));

final dbPipeline = ResiliencePipelineBuilder()
.addStrategy(dbRateLimiter)
.addTimeout(Duration(seconds: 30))
.build();

User-Specific Rate Limiting​

final userRateLimiter = RateLimiterStrategy(RateLimiterStrategyOptions(
permitLimit: 100, // 100 operations per user per hour
window: Duration(hours: 1),
onRateLimitExceeded: (context, args) {
final userId = context.getProperty<String>('userId');
logger.warn('User $userId exceeded rate limit');

// Could implement user-specific penalties
userService.recordRateLimitViolation(userId!);
},
));

// Usage
final context = ResilienceContext();
context.setProperty('userId', 'user123');

await pipeline.execute(
(context) => processUserRequest(userId),
context: context,
);

Bulk Operation Rate Limiting​

final bulkRateLimiter = RateLimiterStrategy(RateLimiterStrategyOptions(
permitLimit: 10, // Only 10 bulk operations per minute
window: Duration(minutes: 1),
queueLimit: 5, // Queue a few bulk operations
onRateLimitExceeded: (context, args) {
final operationType = context.getProperty<String>('operationType');
logger.info('Bulk operation rate limited: $operationType');
},
));

final bulkPipeline = ResiliencePipelineBuilder()
.addStrategy(bulkRateLimiter)
.build();

// Usage for different bulk operations
await bulkPipeline.execute((context) {
context.setProperty('operationType', 'bulk_email');
return sendBulkEmails(emailList);
});

await bulkPipeline.execute((context) {
context.setProperty('operationType', 'bulk_update');
return updateBulkRecords(updates);
});

External Service Rate Limiting​

final externalServiceRateLimiter = RateLimiterStrategy(RateLimiterStrategyOptions(
permitLimit: 500, // 500 calls per 15 minutes (respecting external API limits)
window: Duration(minutes: 15),
queueLimit: 50,
onRateLimitExceeded: (context, args) {
final service = context.getProperty<String>('externalService');
logger.warn('External service rate limit: $service');

// Alert when approaching external service limits
if (shouldAlertExternalRateLimit(service!)) {
alertService.sendAlert('Approaching rate limit for $service');
}
},
));

File Processing Rate Limiting​

final fileProcessingRateLimiter = RateLimiterStrategy(RateLimiterStrategyOptions(
permitLimit: 5, // Process 5 files at a time
window: Duration(seconds: 1),
queueLimit: 100, // Large queue for file processing
onRateLimitExceeded: (context, args) {
final fileName = context.getProperty<String>('fileName');
logger.debug('File processing queued: $fileName');
},
));

final fileProcessingPipeline = ResiliencePipelineBuilder()
.addStrategy(fileProcessingRateLimiter)
.addTimeout(Duration(minutes: 10))
.build();

// Process multiple files with rate limiting
for (final file in filesToProcess) {
final context = ResilienceContext();
context.setProperty('fileName', file.name);

await fileProcessingPipeline.execute(
(context) => processFile(file),
context: context,
);
}

Adaptive Rate Limiting​

class AdaptiveRateLimiter {
int _currentLimit = 100;

RateLimiterStrategy<T> createRateLimiter<T>() {
return RateLimiterStrategy(RateLimiterStrategyOptions(
permitLimit: _currentLimit,
window: Duration(minutes: 1),
onRateLimitExceeded: (context, args) {
// Reduce rate limit if we're hitting it too often
if (shouldReduceLimit()) {
_currentLimit = (_currentLimit * 0.8).round();
logger.info('Reduced rate limit to $_currentLimit');
}
},
));
}

void increaseLimit() {
// Gradually increase limit during low traffic
_currentLimit = (_currentLimit * 1.1).round();
}
}

Rate Limiting with Retry​

final pipeline = ResiliencePipelineBuilder()
.addRateLimiter(RateLimiterStrategyOptions(
permitLimit: 100,
window: Duration(minutes: 1),
))
.addRetry(RetryStrategyOptions(
maxRetryAttempts: 3,
delay: Duration(seconds: 5),
shouldHandle: (outcome) =>
outcome.hasException &&
outcome.exception is RateLimitExceededException,
))
.build();

Multiple Rate Limiters​

// Different rate limits for different operation types
final readRateLimiter = RateLimiterStrategy(RateLimiterStrategyOptions(
permitLimit: 1000, // High limit for reads
window: Duration(minutes: 1),
));

final writeRateLimiter = RateLimiterStrategy(RateLimiterStrategyOptions(
permitLimit: 100, // Lower limit for writes
window: Duration(minutes: 1),
));

// Use in different pipelines
final readPipeline = ResiliencePipelineBuilder()
.addStrategy(readRateLimiter)
.build();

final writePipeline = ResiliencePipelineBuilder()
.addStrategy(writeRateLimiter)
.build();

Sliding Window Rate Limiting​

final slidingWindowRateLimiter = RateLimiterStrategy(RateLimiterStrategyOptions(
permitLimit: 60, // 60 operations per minute
window: Duration(minutes: 1), // Sliding window
onRateLimitExceeded: (context, args) {
final timestamp = DateTime.now();
logger.debug('Rate limit exceeded at $timestamp, retry after ${args.retryAfter}');
},
));

Best Practices​

Choose Appropriate Limits​

// For user-facing APIs - generous limits
RateLimiterStrategyOptions(
permitLimit: 1000,
window: Duration(hours: 1),
)

// For expensive operations - strict limits
RateLimiterStrategyOptions(
permitLimit: 10,
window: Duration(minutes: 1),
)

// For bulk operations - very strict limits
RateLimiterStrategyOptions(
permitLimit: 1,
window: Duration(minutes: 5),
)

Use Queueing Wisely​

// For interactive operations - no queueing
RateLimiterStrategyOptions(
permitLimit: 100,
queueLimit: 0, // Fail fast for user interactions
)

// For background operations - large queue
RateLimiterStrategyOptions(
permitLimit: 10,
queueLimit: 1000, // Large queue for background work
)

Monitor Rate Limit Usage​

RateLimiterStrategyOptions(
permitLimit: 100,
onRateLimitExceeded: (context, args) {
final operation = context.getProperty<String>('operation');

// Track rate limit metrics
metrics.incrementCounter('rate_limit_exceeded', {
'operation': operation,
});

// Track retry-after times
metrics.recordHistogram('rate_limit_retry_after',
args.retryAfter.inSeconds);

// Alert if rate limits are frequently exceeded
if (isFrequentlyExceeded(operation)) {
alertService.sendAlert('Frequent rate limit violations: $operation');
}
},
)

Combine with Circuit Breakers​

final pipeline = ResiliencePipelineBuilder()
.addRateLimiter(RateLimiterStrategyOptions(
permitLimit: 100,
window: Duration(minutes: 1),
))
.addCircuitBreaker(CircuitBreakerStrategyOptions(
failureRatio: 0.5,
minimumThroughput: 10,
))
.build();

Handle Rate Limit Exceptions​

try {
final result = await pipeline.execute(
operation,
context: ResilienceContext(operationKey: 'rate-limited-operation'),
);
return result;
} on RateLimitExceededException catch (e) {
// Handle rate limit specifically
logger.warn('Rate limit exceeded, retry after ${e.retryAfter}');

// Could implement exponential backoff
await Future.delayed(e.retryAfter);

// Or return cached data
return getCachedResult();
}

Test Rate Limiting​

test('should enforce rate limits', () async {
final rateLimiter = RateLimiterStrategy(RateLimiterStrategyOptions(
permitLimit: 2,
window: Duration(seconds: 1),
));

// First two calls should succeed
await pipeline.execute(
mockOperation,
context: ResilienceContext(operationKey: 'test-operation'),
);
await pipeline.execute(
mockOperation,
context: ResilienceContext(operationKey: 'test-operation'),
);

// Third call should be rate limited
expect(
() => pipeline.execute(
mockOperation,
context: ResilienceContext(operationKey: 'test-operation'),
),
throwsA(isA<RateLimitExceededException>()),
);
});

Consider Time Zones and Peaks​

// Adjust rate limits based on time of day
RateLimiterStrategyOptions(
permitLimit: isPeakHours() ? 50 : 100,
window: Duration(minutes: 1),
)

// Or use different rate limiters for different times
final peakHoursRateLimiter = RateLimiterStrategy(RateLimiterStrategyOptions(
permitLimit: 50,
window: Duration(minutes: 1),
));

final offPeakRateLimiter = RateLimiterStrategy(RateLimiterStrategyOptions(
permitLimit: 200,
window: Duration(minutes: 1),
));