-
Notifications
You must be signed in to change notification settings - Fork 1
RateLimit Policy
Unit: Murphy.Policy.RateLimit
The Rate Limit policy controls the rate of operations to prevent system overload using a token bucket algorithm.
Rate limiting prevents:
- API throttling (exceeding third-party rate limits)
- Resource exhaustion (too many concurrent operations)
- System overload (protecting your own services)
- Uncontrolled bursts of activity
- ERateLimitRejectedException Class
- IRateLimitPolicy Interface
- TRateLimitPolicy Class
- TRateLimitBuilder Class
- Usage Examples
Exception thrown when the rate limit has been exceeded.
type
ERateLimitRejectedException = class(Exception)
end;Description: Simple exception class indicating that the rate limit was exceeded.
Usage:
try
RateLimitPolicy.Execute(procedure begin MakeAPICall; end);
except
on E: ERateLimitRejectedException do
begin
WriteLn('Rate limit exceeded - too many calls');
// Wait and retry, or handle appropriately
end;
end;The interface for the Rate Limit policy pattern.
type
IRateLimitPolicy = interface(IPolicy)
['{B4AFFB0E-C958-4D89-A2BC-79659915581E}']
function Allow(ACalls: Integer): IRateLimitPolicy;
procedure Execute(AProc: TProc);
function Within(ADuration: TTimeSpan): IRateLimitPolicy;
end;function Allow(ACalls: Integer): IRateLimitPolicy;Description: Configures the maximum number of calls allowed within the time window.
Parameters:
-
ACalls: Integer- Maximum number of calls allowed
Returns: IRateLimitPolicy - Self for method chaining
Default: 20 calls
Example:
// Allow 100 calls
Policy := TRateLimitBuilder
.Handle(Exception) // Exception type doesn't affect rate limiting
.Allow(100)
.Within(TTimeSpan.FromMinutes(1))
.Build;procedure Execute(AProc: TProc);Description: Executes the provided procedure if rate limit allows, otherwise throws ERateLimitRejectedException.
Parameters:
-
AProc: TProc- Anonymous procedure to execute
Behavior:
- Checks if token bucket has expired (time window passed)
- If expired: Refills bucket with tokens
- If tokens available: Consumes one token and executes
AProc - If no tokens available: Throws
ERateLimitRejectedException
Example:
try
RateLimitPolicy.Execute(
procedure
begin
WriteLn('Making API call...');
CallAPI;
end);
except
on E: ERateLimitRejectedException do
WriteLn('Rate limit exceeded!');
end;function Within(ADuration: TTimeSpan): IRateLimitPolicy;Description: Configures the time window for the rate limit.
Parameters:
-
ADuration: TTimeSpan- Time window duration
Returns: IRateLimitPolicy - Self for method chaining
Default: 1 second
Example:
// 100 calls per minute
Policy := TRateLimitBuilder
.Handle(Exception)
.Allow(100)
.Within(TTimeSpan.FromMinutes(1))
.Build;
// 10 calls per second
Policy := TRateLimitBuilder
.Handle(Exception)
.Allow(10)
.Within(TTimeSpan.FromSeconds(1))
.Build;
// 1000 calls per hour
Policy := TRateLimitBuilder
.Handle(Exception)
.Allow(1000)
.Within(TTimeSpan.FromHours(1))
.Build;Concrete implementation of the Rate Limit policy using token bucket algorithm.
type
TRateLimitPolicy = class(TPolicy, IRateLimitPolicy)
private
FAllowedCalls: Integer;
FBucketIds: TStack<TGUID>;
FBucketTime: TDateTime;
FWithinDuration: TTimeSpan;
protected
function IsBucketExpired: Boolean;
public
constructor Create(AExceptionTypes: TArray<ExceptClass>); override;
destructor Destroy; override;
function Allow(ACalls: Integer): IRateLimitPolicy;
procedure Execute(AProc: TProc);
function Within(ADuration: TTimeSpan): IRateLimitPolicy;
end;constructor Create(AExceptionTypes: TArray<ExceptClass>); override;Defaults:
-
FAllowedCalls:20 -
FWithinDuration:1 second -
FBucketTime:MinDateTime(will trigger immediate bucket fill) -
FBucketIds: Empty stack
function IsBucketExpired: Boolean;Description: Checks if the current token bucket time window has expired.
Returns: Boolean
-
True- Time window has expired, bucket should be refilled -
False- Still within current time window
Implementation (Murphy.Policy.RateLimit.pas:105):
Result := (FBucketTime + FWithinDuration) < Now;The implementation uses a simple token bucket:
-
Bucket Creation: When expired or first use, fill bucket with
FAllowedCallstokens (GUIDs) -
Token Consumption: Each
Executecall pops one token from the bucket -
Rejection: If bucket is empty, throw
ERateLimitRejectedException - Refill: When time window expires, clear and refill bucket
Characteristics:
- Allows bursts (can use all tokens immediately)
- Simple and predictable
- No gradual token replenishment (refills completely when window expires)
Builder class for creating rate limit policies.
type
TRateLimitBuilder = class sealed(TPolicyBuilder<IRateLimitPolicy>)
public
class function Handle(AExceptionTypes: TArray<ExceptClass>): IRateLimitPolicy; override;
end;class function Handle(AExceptionTypes: TArray<ExceptClass>): IRateLimitPolicy; override;Description: Creates a rate limit policy.
Note: Exception type parameter is not used by rate limiting logic (inherited from base), but provided for consistency.
Usage:
Policy := TRateLimitBuilder
.Handle(Exception) // Can use any exception type
.Allow(50)
.Within(TTimeSpan.FromMinutes(1))
.Build;var
Policy: IRateLimitPolicy;
I: Integer;
begin
// Allow 5 calls per 10 seconds
Policy := TRateLimitBuilder
.Handle(Exception)
.Allow(5)
.Within(TTimeSpan.FromSeconds(10))
.Build;
for I := 1 to 10 do
begin
try
Policy.Execute(
procedure
begin
WriteLn(Format('Call %d at %s', [I, TimeToStr(Now)]));
MakeAPICall;
end);
except
on E: ERateLimitRejectedException do
begin
WriteLn(Format('Call %d rejected - rate limit exceeded', [I]));
Sleep(2000); // Wait before retrying
end;
end;
end;
end;type
TAPIClient = class
private
FRateLimitPolicy: IRateLimitPolicy;
public
constructor Create;
function Get(const AURL: string): string;
end;
constructor TAPIClient.Create;
begin
// GitHub API: 60 requests per hour for unauthenticated requests
FRateLimitPolicy := TRateLimitBuilder
.Handle(Exception)
.Allow(60)
.Within(TTimeSpan.FromHours(1))
.Build;
end;
function TAPIClient.Get(const AURL: string): string;
begin
try
FRateLimitPolicy.Execute(
procedure
begin
Result := HTTP.Get(AURL);
end);
except
on E: ERateLimitRejectedException do
begin
raise Exception.Create('API rate limit exceeded. Please wait before making more requests.');
end;
end;
end;var
Policy: IRateLimitPolicy;
MaxRetries: Integer;
RetryCount: Integer;
begin
Policy := TRateLimitBuilder
.Handle(Exception)
.Allow(10)
.Within(TTimeSpan.FromSeconds(1))
.Build;
MaxRetries := 5;
RetryCount := 0;
while RetryCount < MaxRetries do
begin
try
Policy.Execute(
procedure
begin
ProcessItem;
end);
Break; // Success - exit retry loop
except
on E: ERateLimitRejectedException do
begin
Inc(RetryCount);
WriteLn(Format('Rate limited. Retry %d/%d...', [RetryCount, MaxRetries]));
Sleep(200); // Wait 200ms before retry
end;
end;
end;
if RetryCount >= MaxRetries then
raise Exception.Create('Failed after maximum retries due to rate limiting');
end;var
PerSecondPolicy: IRateLimitPolicy;
PerMinutePolicy: IRateLimitPolicy;
begin
// Twitter-style: 15 calls per 15 minutes AND max 1 per second
PerSecondPolicy := TRateLimitBuilder
.Handle(Exception)
.Allow(1)
.Within(TTimeSpan.FromSeconds(1))
.Build;
PerMinutePolicy := TRateLimitBuilder
.Handle(Exception)
.Allow(15)
.Within(TTimeSpan.FromMinutes(15))
.Build;
// Apply both limits
try
PerSecondPolicy.Execute(
procedure
begin
PerMinutePolicy.Execute(
procedure
begin
MakeAPICall;
end);
end);
except
on E: ERateLimitRejectedException do
WriteLn('Rate limit exceeded');
end;
end;procedure ProcessItemsWithRateLimit(Items: TList<string>);
var
Policy: IRateLimitPolicy;
Item: string;
ProcessedCount: Integer;
begin
// Process max 100 items per minute
Policy := TRateLimitBuilder
.Handle(Exception)
.Allow(100)
.Within(TTimeSpan.FromMinutes(1))
.Build;
ProcessedCount := 0;
for Item in Items do
begin
try
Policy.Execute(
procedure
begin
WriteLn(Format('Processing: %s', [Item]));
ProcessItem(Item);
Inc(ProcessedCount);
end);
except
on E: ERateLimitRejectedException do
begin
WriteLn(Format('Rate limit reached after %d items. Waiting...', [ProcessedCount]));
Sleep(60000); // Wait 1 minute for bucket to reset
// Retry this item
Policy.Execute(procedure begin ProcessItem(Item); end);
end;
end;
end;
WriteLn(Format('Processed %d items', [ProcessedCount]));
end;The current implementation:
- Refills completely when time window expires (not gradually)
- Allows bursts - all tokens can be consumed immediately
- Resets on expiration - doesn't carry over unused tokens
Example:
// Allow 10 calls per minute
Policy := TRateLimitBuilder.Handle(Exception).Allow(10).Within(TTimeSpan.FromMinutes(1)).Build;
// You can make 10 calls immediately (burst)
for I := 1 to 10 do
Policy.Execute(procedure begin CallAPI; end); // All succeed
// 11th call fails
Policy.Execute(procedure begin CallAPI; end); // Throws ERateLimitRejectedException
// After 1 minute, bucket refills and you can make 10 more callsCurrent Status: Rate limit policy is not thread-safe.
Recommendations:
- Use separate policy instances per thread
- Or protect shared instance with synchronization primitives (TMonitor, TCriticalSection)
Rate limiting works well with:
- Retry: Retry after delay when rate limit exceeded
- Circuit Breaker: Prevent overwhelming rate-limited services
- Fallback: Provide cached data when rate limit prevents fresh data fetch
- Rate Limit Pattern Guide - Comprehensive usage guide
- Combining Patterns - Using rate limit with other policies
- Best Practices - Production recommendations
- Base Policy API - Inherited functionality