Token bucket and sliding window rate limiting for async operations
dart pub add philiprehberger_rate_limiterToken bucket, sliding window, and fixed window rate limiting for async operations
Add to your pubspec.yaml:
dependencies:
philiprehberger_rate_limiter: ^0.6.0
Then run:
dart pub get
import 'package:philiprehberger_rate_limiter/philiprehberger_rate_limiter.dart';
final limiter = TokenBucket(
capacity: 10,
refillInterval: Duration(seconds: 1),
);
if (limiter.tryAcquire()) {
// Request allowed
}
The token bucket algorithm maintains a bucket of tokens that refills at a fixed rate. Each request consumes one token.
final bucket = TokenBucket(
capacity: 5,
refillInterval: Duration(milliseconds: 200),
);
// Non-blocking check
if (bucket.tryAcquire()) {
print('Allowed');
} else {
print('Rate limited');
}
// Async wait until a token is available
await bucket.acquire();
The sliding window algorithm tracks request timestamps and limits the number of requests within a rolling time window.
final window = SlidingWindow(
maxRequests: 100,
window: Duration(minutes: 1),
);
if (window.tryAcquire()) {
print('Allowed');
}
// Wait until the window has room
await window.acquire();
The fixed window algorithm allows up to a set number of requests within each fixed time interval. The counter resets completely when the window expires.
final limiter = FixedWindow(
maxRequests: 10,
window: Duration(seconds: 1),
);
if (limiter.tryAcquire()) {
print('Allowed');
}
// Wait until the next window
await limiter.acquire();
All algorithms support per-key rate limiting for multi-tenant scenarios.
final limiter = TokenBucket(
capacity: 5,
refillInterval: Duration(seconds: 1),
);
limiter.tryAcquire(key: 'user-123');
limiter.tryAcquire(key: 'user-456');
limiter.reset(key: 'user-123');
Track request counts with the stats() method available on all rate limiters.
final limiter = TokenBucket(
capacity: 5,
refillInterval: Duration(seconds: 1),
);
limiter.tryAcquire();
limiter.tryAcquire();
final s = limiter.stats();
print('Total: ${s.totalRequests}');
print('Allowed: ${s.allowedRequests}');
print('Rejected: ${s.rejectedRequests}');
Check remaining capacity without consuming a permit.
final limiter = FixedWindow(
maxRequests: 10,
window: Duration(seconds: 1),
);
print(limiter.availablePermits()); // 10
limiter.tryAcquire();
print(limiter.availablePermits()); // 9
Quickly check whether a rate limiter has any permits left.
final limiter = TokenBucket(
capacity: 1,
refillInterval: Duration(seconds: 1),
);
print(limiter.isExhausted()); // false
limiter.tryAcquire();
print(limiter.isExhausted()); // true
Check how long to wait before the next permit becomes available. Returns null if a permit is already available.
final limiter = TokenBucket(
capacity: 10,
refillInterval: Duration(seconds: 1),
);
// Exhaust permits...
final wait = limiter.retryAfter();
if (wait != null) {
print('Retry after ${wait.inMilliseconds}ms');
}
Combine multiple rate limiters to enforce multi-tier limits. A request is only allowed when all inner limiters permit it.
final limiter = CompositeRateLimiter([
TokenBucket(capacity: 10, refillInterval: Duration(seconds: 1)),
SlidingWindow(maxRequests: 1000, window: Duration(hours: 1)),
]);
if (limiter.tryAcquire()) {
// Passed both the per-second and per-hour limit
}
Pass a timeout to acquire() to throw a TimeoutException if a permit is not available in time.
import 'dart:async';
try {
await limiter.acquire(timeout: Duration(seconds: 5));
} on TimeoutException {
print('Could not acquire permit in time');
}
Clean up rate limiter resources when they are no longer needed:
final limiter = TokenBucket(
capacity: 10,
refillInterval: Duration(seconds: 1),
);
// Use the limiter...
limiter.tryAcquire();
// Clean up when done
limiter.dispose();
print(limiter.isDisposed); // true
// Throws StateError after dispose
limiter.tryAcquire(); // StateError: RateLimiter has been disposed
| Method | Description |
|---|---|
TokenBucket({capacity, refillInterval}) | Create a token bucket rate limiter |
SlidingWindow({maxRequests, window}) | Create a sliding window rate limiter |
FixedWindow({maxRequests, window}) | Create a fixed window rate limiter |
CompositeRateLimiter(limiters) | Create a composite rate limiter from multiple limiters |
RateLimiter.tryAcquire({key}) | Try to acquire a permit, returns true if allowed |
RateLimiter.acquire({key, timeout}) | Acquire a permit, waiting if necessary; throws TimeoutException on timeout |
RateLimiter.reset({key}) | Reset state for a key, or all keys if omitted |
RateLimiter.stats({key}) | Get request statistics (total, allowed, rejected) |
RateLimiter.availablePermits({key}) | Check remaining permits without consuming one |
RateLimiter.isExhausted({key}) | Returns true when no permits are available |
RateLimiter.retryAfter({key}) | Returns duration until next permit, or null if available |
RateLimiter.dispose() | Release all resources; subsequent acquire calls throw StateError |
RateLimiter.isDisposed | Whether the rate limiter has been disposed |
dart pub get
dart analyze --fatal-infos
dart test
If you find this project useful: