TsgcWSRateLimiter

TsgcWSRateLimiter implements a full-featured rate limiting component that protects server endpoints from excessive traffic, abuse and scraping.

Introduction

TsgcWSRateLimiter implements a full-featured rate limiting component that protects server endpoints from excessive traffic, abuse and scraping. It supports three algorithms (Token Bucket, Sliding Window, Fixed Window), scoped rules (per IP, per API key, per user, per endpoint), long-term quotas (hour / day / month), burst protection, a configurable HTTP 429 response shape and real-time statistics. Internal state can be persisted to a file or stream so counters survive restarts.

 

The rate limiter integrates automatically with server components through the RateLimiter property available on TsgcWebSocketHTTPServer and TsgcWSServer_HTTPAPI. Once assigned, it evaluates every connection and every message without requiring manual event wiring.

 

Quick Start

 

1. Drop a TsgcWSRateLimiter component on the form.

 

2. Enable one or more strategies and scopes (see the per-section documentation below).

 

3. Assign the rate limiter to a server component:

 

sgcWebSocketHTTPServer1.RateLimiter := sgcWSRateLimiter1;

 

4. Start the server. The rate limiter automatically evaluates every connection and message, rejects excess traffic with HTTP 429 and updates its statistics counters.

 

Top-level Properties

 

What it does: Master switch for the component and optional storage file used by the state-persistence methods. When Enabled is False every public method short-circuits to "allowed" and no counters are updated.

 

When to use it: Disable the rate limiter at runtime during a controlled load test or during a maintenance window without unassigning it from the server. Set StorageFile so counters survive an application restart — for example a rolling Token Bucket continues refilling from where it stopped instead of starting fresh and accepting a huge burst.

 

PropertyTypeDefaultDescription
EnabledBooleanTrueMaster switch. When False, IsAllowed / Consume / IsConnectionAllowed / IsMessageAllowed always return True and no counters are updated.
StorageFilestring‘’Optional file path used by SaveStateToFile / LoadStateFromFile for counter persistence across restarts.

 


    sgcWSRateLimiter1.Enabled := True;
    sgcWSRateLimiter1.StorageFile := 'ratelimit.dat';
    sgcWSRateLimiter1.LoadStateFromFile(sgcWSRateLimiter1.StorageFile);
    

 

TokenBucket Strategy

 

What it does: Maintains a bucket of up to Capacity tokens per key. The bucket refills at RefillRate tokens every RefillIntervalMs milliseconds. Each request consumes one token (or the Cost argument passed to Consume). When the bucket is empty the request is rejected and a RetryAfterSec hint is computed from the refill schedule.

 

When to use it: The API advertises an average rate of 10 req/sec but you want to let well-behaved clients burst up to 50 requests back-to-back without being throttled. A Token Bucket with Capacity=50 and RefillRate=10 per 1000 ms lets a client send 50 requests instantly, then forces them down to 10 req/sec until the bucket refills.

 

PropertyTypeDefaultDescription
EnabledBooleanFalseEnables the Token Bucket algorithm as the global default strategy.
CapacityInteger100Maximum number of tokens the bucket can hold. Controls the maximum burst size.
RefillRateInteger10Number of tokens added to the bucket every RefillIntervalMs.
RefillIntervalMsInteger1000Refill period in milliseconds. Together with RefillRate determines the sustained request rate.

 


    // Average 10 req/sec, bursts up to 50
    sgcWSRateLimiter1.TokenBucket.Enabled := True;
    sgcWSRateLimiter1.TokenBucket.Capacity := 50;
    sgcWSRateLimiter1.TokenBucket.RefillRate := 10;
    sgcWSRateLimiter1.TokenBucket.RefillIntervalMs := 1000;
    // First 50 requests succeed immediately; afterwards throttled to 10/sec until refill
    

 

SlidingWindow Strategy

 

What it does: Stores a timestamp for every request and counts the timestamps that fall within a rolling window of WindowSec seconds. Rejects when the count would exceed MaxRequests. The window slides continuously, so there is no hard reset moment where a burst can slip through.

 

When to use it: You publish a strict SLA guaranteeing "no more than 100 requests in any 60-second rolling window". Fixed Window would let a client fire 100 requests at 12:00:59 and another 100 at 12:01:00 (200 within one second). Sliding Window prevents that and enforces the SLA exactly.

 

PropertyTypeDefaultDescription
EnabledBooleanFalseEnables the Sliding Window algorithm as the global default strategy.
WindowSecInteger60Size of the rolling window in seconds.
MaxRequestsInteger1000Maximum number of requests permitted within any WindowSec-wide rolling window.

 


    // Strict SLA: max 100 requests in any rolling 60 seconds
    sgcWSRateLimiter1.SlidingWindow.Enabled := True;
    sgcWSRateLimiter1.SlidingWindow.WindowSec := 60;
    sgcWSRateLimiter1.SlidingWindow.MaxRequests := 100;
    // Client that sends a burst of 100 requests near t=59s will be throttled at t=60s
    

 

FixedWindow Strategy

 

What it does: Counts requests within a fixed clock window of WindowSec seconds and resets the counter to zero at the window boundary. The simplest algorithm and the cheapest memory-wise; however it can allow up to 2×MaxRequests in a short period around a boundary crossing.

 

When to use it: Billing-style quotas like "1000 requests per hour, counter resets on the hour". Customers expect a clearly visible reset time, and the minor boundary-burst behavior is acceptable because the billing unit is the hour, not the rolling minute.

 

PropertyTypeDefaultDescription
EnabledBooleanFalseEnables the Fixed Window algorithm as the global default strategy.
WindowSecInteger60Size of the fixed window in seconds. The counter resets to 0 when the window elapses.
MaxRequestsInteger1000Maximum number of requests permitted within a single fixed window.

 


    // Billing-style: 1000 requests per hour, resets on the hour
    sgcWSRateLimiter1.FixedWindow.Enabled := True;
    sgcWSRateLimiter1.FixedWindow.WindowSec := 3600;
    sgcWSRateLimiter1.FixedWindow.MaxRequests := 1000;
    

 

PerIP

 

What it does: Defines a rate limit scoped to the source IP address. When a request key does not match PerAPIKey / PerUser / PerEndpoint, PerIP becomes the active rule and overrides the global Token/Sliding/Fixed defaults. Each IP gets its own independent counter.

 

When to use it: A public API with many clients. Without PerIP a single noisy client can consume the global budget and starve everyone else. Give every IP its own bucket (for example 100 requests per minute) so one client cannot affect others.

 

PropertyTypeDefaultDescription
EnabledBooleanFalseEnables per-IP rate limiting.
MaxRequestsInteger100Maximum requests per window (or bucket capacity when Strategy is rlsTokenBucket).
WindowSecInteger60Window size in seconds. For Token Bucket also used to derive the refill rate (MaxRequests / WindowSec tokens per second).
StrategyTsgcRateLimitStrategyrlsTokenBucketAlgorithm used for this scope: rlsTokenBucket, rlsSliding or rlsFixed.

 


    // Every client IP gets its own 100 req/min bucket
    sgcWSRateLimiter1.PerIP.Enabled := True;
    sgcWSRateLimiter1.PerIP.Strategy := rlsTokenBucket;
    sgcWSRateLimiter1.PerIP.MaxRequests := 100;
    sgcWSRateLimiter1.PerIP.WindowSec := 60;
    

 

PerAPIKey

 

What it does: Same semantics as PerIP but scoped to an API-key identifier. The component recognizes keys that start with the prefix apikey: as API-key-scoped. Designed to integrate with TsgcWSAPIKeyManager so every customer gets an independent rate budget regardless of source IP.

 

When to use it: SaaS with tiered pricing. Every customer has their own API key and their own quota independent of where they connect from. Use PerAPIKey as the default tier and differentiate customer tiers (Free / Pro / Enterprise) either through a PerEndpoint rule keyed by path prefix or by overriding the decision inside the OnThrottled event handler.

 

PropertyTypeDefaultDescription
EnabledBooleanFalseEnables per-API-key rate limiting.
MaxRequestsInteger100Maximum requests per window (or bucket capacity for Token Bucket).
WindowSecInteger60Window size in seconds.
StrategyTsgcRateLimitStrategyrlsTokenBucketAlgorithm used for this scope.

 


    // Default API-key tier: 1000 req/min
    sgcWSRateLimiter1.PerAPIKey.Enabled := True;
    sgcWSRateLimiter1.PerAPIKey.Strategy := rlsSliding;
    sgcWSRateLimiter1.PerAPIKey.MaxRequests := 1000;
    sgcWSRateLimiter1.PerAPIKey.WindowSec := 60;
    // Tier-differentiation can be done via PerEndpoint rules or OnThrottled
    

 

PerUser

 

What it does: Same structure as PerIP / PerAPIKey but keyed by an application-defined user identifier. The component recognizes keys that start with the prefix user: as user-scoped. Useful when a single user may connect from multiple devices or IPs and you want a single shared budget for that user.

 

When to use it: Authenticated web / mobile app. User alice@example.com is allowed 1000 requests per day regardless of whether she calls from her phone, laptop or work desktop. Aggregating by IP would double-count; aggregating by user gives the budget you actually want to bill against.

 

PropertyTypeDefaultDescription
EnabledBooleanFalseEnables per-user rate limiting.
MaxRequestsInteger100Maximum requests per window (or bucket capacity for Token Bucket).
WindowSecInteger60Window size in seconds.
StrategyTsgcRateLimitStrategyrlsTokenBucketAlgorithm used for this scope.

 


    // 1000 requests/day per authenticated user across all their devices
    sgcWSRateLimiter1.PerUser.Enabled := True;
    sgcWSRateLimiter1.PerUser.Strategy := rlsFixed;
    sgcWSRateLimiter1.PerUser.MaxRequests := 1000;
    sgcWSRateLimiter1.PerUser.WindowSec := 86400;
    // Pass keys like 'user:alice@example.com' to Consume / IsAllowed
    

 

PerEndpoint

 

What it does: Collection of pattern-based rules. Each item has its own Pattern (wildcards * and ? supported), Strategy, MaxRequests and WindowSec. The first enabled item whose pattern matches the key wins — no further items are evaluated. Takes precedence over PerAPIKey, PerUser and PerIP.

 

When to use it: You have an expensive endpoint /api/v1/expensive-report that must be limited to 10 req/min per client, while a cheap status endpoint /api/v1/status allows 1000 req/min. Different endpoints have different budgets — configure one PerEndpoint item per path pattern.

 

PropertyTypeDefaultDescription
EnabledBooleanFalseEnables the PerEndpoint rule set. When False no items are evaluated.
RulesTCollectionemptyCollection of TsgcRateLimitRuleItem. Order matters — first match wins.

 

Each TsgcRateLimitRuleItem exposes:

 

PropertyTypeDefaultDescription
Namestring‘’Friendly name shown in the object inspector.
EnabledBooleanTrueDisables this item without deleting it.
Patternstring‘’Wildcard pattern matched against the key (case-insensitive). Supports * and ?.
MaxRequestsInteger100Maximum requests per window / bucket capacity.
WindowSecInteger60Window size in seconds.
StrategyTsgcRateLimitStrategyrlsTokenBucketAlgorithm to use for matched requests.

 


    sgcWSRateLimiter1.PerEndpoint.Enabled := True;

    with sgcWSRateLimiter1.PerEndpoint.Rules.Add as TsgcRateLimitRuleItem do
    begin
      Name := 'expensive-report';
      Pattern := '*/api/v1/expensive-report*';
      Strategy := rlsSliding;
      MaxRequests := 10;
      WindowSec := 60;
    end;

    with sgcWSRateLimiter1.PerEndpoint.Rules.Add as TsgcRateLimitRuleItem do
    begin
      Name := 'status';
      Pattern := '*/api/v1/status*';
      Strategy := rlsTokenBucket;
      MaxRequests := 1000;
      WindowSec := 60;
    end;
    

 

Quotas

 

What it does: Long-term caps that persist beyond the rolling / refilling window of the main strategy. Each item caps how many requests a key (or the whole system) may make within a calendar period (qpHour, qpDay or qpMonth). Once the Limit is reached the request is rejected and the OnQuotaExceeded event fires.

 

When to use it: Free-tier plan limited to 10,000 requests per month hard cap. The rolling token bucket still smooths short-term traffic, but when the monthly quota is exhausted every further call fails until the next calendar month.

 

PropertyTypeDefaultDescription
EnabledBooleanFalseEnables the quota rule set. When False no items are evaluated.
QuotasTCollectionemptyCollection of TsgcRateLimitQuotaItem. All enabled items are evaluated in order.

 

Each TsgcRateLimitQuotaItem exposes:

 

PropertyTypeDefaultDescription
Namestring‘’Friendly name reported in OnQuotaExceeded and in Result.Reason ("quota:Name").
EnabledBooleanTrueDisables this quota without deleting it.
ScopeTsgcRateLimitQuotaScopeqscPerKeyqscPerKey counts independently per key; qscGlobal counts across all keys combined.
PeriodTsgcRateLimitQuotaPeriodqpHourqpHour, qpDay or qpMonth. Counter resets automatically at the start of each period.
LimitInt6410000Maximum requests permitted within the period.

 


    sgcWSRateLimiter1.Quotas.Enabled := True;

    with sgcWSRateLimiter1.Quotas.Quotas.Add as TsgcRateLimitQuotaItem do
    begin
      Name := 'free-tier-monthly';
      Scope := qscPerKey;
      Period := qpMonth;
      Limit := 10000;
    end;
    

 

BurstProtection

 

What it does: A short-timescale spike detector that runs in parallel with the main strategy. If more than BurstThreshold requests arrive from the same key within BurstWindowMs milliseconds, the key is placed in a cooldown for CooldownSec seconds during which every request is rejected — even if the main strategy would have allowed them.

 

When to use it: Detect scraper and scanner clients that flood a server with 100+ requests in 100 ms bursts but stay under a per-minute limit. Burst protection catches this machine-gun pattern that a human user never produces.

 

PropertyTypeDefaultDescription
EnabledBooleanFalseEnables the burst detector.
BurstThresholdInteger50Number of requests within BurstWindowMs that triggers a cooldown.
BurstWindowMsInteger500Burst observation window in milliseconds.
CooldownSecInteger30Duration of the cooldown period after a burst is detected. During cooldown every request is rejected.

 


    // More than 50 requests in 500ms triggers a 30-second cooldown
    sgcWSRateLimiter1.BurstProtection.Enabled := True;
    sgcWSRateLimiter1.BurstProtection.BurstThreshold := 50;
    sgcWSRateLimiter1.BurstProtection.BurstWindowMs := 500;
    sgcWSRateLimiter1.BurstProtection.CooldownSec := 30;
    

 

Response

 

What it does: Configures the shape of the HTTP response returned to clients when a request is throttled. Controls the status code, the body message and whether Retry-After and X-RateLimit-* headers are added to the response.

 

When to use it: You want well-behaved clients to back off correctly. Return the standard 429 status with a Retry-After header indicating how long they should wait, and include X-RateLimit-Limit / X-RateLimit-Remaining / X-RateLimit-Reset so clients can pace themselves proactively instead of reacting to rejections.

 

PropertyTypeDefaultDescription
StatusCodeInteger429HTTP status code returned for throttled requests. Use 429 (standard) or 503 for maintenance-style rejection.
Messagestring"Too Many Requests"Body of the HTTP response for throttled requests.
RetryAfterHeaderBooleanTrueWhen True adds a Retry-After header with the computed retry delay in seconds.
IncludeRateLimitHeadersBooleanTrueWhen True adds X-RateLimit-Limit / X-RateLimit-Remaining / X-RateLimit-Reset headers to every response.

 


    sgcWSRateLimiter1.Response.StatusCode := 429;
    sgcWSRateLimiter1.Response.Message := 'Too Many Requests — please slow down';
    sgcWSRateLimiter1.Response.RetryAfterHeader := True;
    sgcWSRateLimiter1.Response.IncludeRateLimitHeaders := True;
    

 

Stats

 

What it does: Read-only counter object that exposes live statistics about the rate limiter. All counters are updated atomically inside the component's internal critical section.

 

When to use it: Live dashboard in an admin UI. Poll Stats from a TTimer and paint the current TotalRequests / TotalThrottled / ActiveKeys in labels, gauges or charts so operators can see traffic pressure in real time.

 

PropertyTypeDescription
TotalRequestsInt64Total number of requests evaluated since the last Reset.
TotalThrottledInt64Total number of requests rejected by the strategy or burst protection.
TotalQuotaExceededInt64Total number of requests rejected because a quota was reached.
ActiveKeysIntegerNumber of keys currently tracked (buckets + window counters + active connections).
UptimeInt64Seconds since the stats object was last reset.

 


    // Poll from a TTimer and update a dashboard
    procedure TForm1.StatsTimer(Sender: TObject);
    begin
      LabelRequests.Caption := Format('Requests: %d', [sgcWSRateLimiter1.Stats.TotalRequests]);
      LabelThrottled.Caption := Format('Throttled: %d', [sgcWSRateLimiter1.Stats.TotalThrottled]);
      LabelActiveKeys.Caption := Format('Active keys: %d', [sgcWSRateLimiter1.Stats.ActiveKeys]);
      LabelUptime.Caption := Format('Uptime: %d s', [sgcWSRateLimiter1.Stats.Uptime]);
    end;
    

 

Events

 

OnThrottled: Fired when a request is rejected by the strategy or burst protection. Provides the key (IP, API key or user) and the reason string ("token_bucket", "sliding_window", "fixed_window", "burst", "connection_rate" or "message_rate"). The Allow parameter (var Boolean) lets you override the decision and let the request through anyway.

 

OnQuotaExceeded: Fired when a long-term quota is reached. Provides the key and the Name of the quota that was exceeded.

 

OnStateChange: Fired when the Token Bucket size changes for a key. Provides the key together with the old and new bucket sizes. Useful for telemetry, debugging and real-time visualizations.

 

Public Methods

 

MethodDescription
IsAllowed(Key, Cost)Returns True if a request of the given Cost (default 1) is allowed for the specified key.
Consume(Key, Cost)Attempts to consume Cost tokens and returns a TsgcRateLimitResult (Allowed, Remaining, RetryAfterSec, Reason).
Reset(Key)Clears the counters for the specified key.
ResetAllClears all internal counters for every tracked key and resets the Stats object.
GetRemaining(Key)Returns the number of requests still available for the specified key.
GetRetryAfter(Key)Returns the number of seconds the caller should wait before retrying.
SaveStateToFile(FileName)Persists all buckets and counters to the given file.
LoadStateFromFile(FileName)Restores buckets and counters previously saved to a file.
SaveStateToStream(Stream)Persists the internal state to an arbitrary stream.
LoadStateFromStream(Stream)Restores the internal state from an arbitrary stream.
IsConnectionAllowed(IP)Server-integration hook. Returns True if a new connection from IP passes rate limiting. Called automatically by the server.
IsMessageAllowed(IP, Message)Server-integration hook. Returns True if a message from IP passes rate limiting. Called automatically by the server.
RegisterConnection(IP)Registers a new connection for tracking. Called automatically.
UnregisterConnection(IP)Removes a connection from tracking. Called automatically on disconnect.

 

Automatic Integration

 

When assigned to a server's RateLimiter property, the component automatically:

 

Complete Example

 


    // Enable rate limiter
    sgcWSRateLimiter1.Enabled := True;

    // Global Token Bucket: 50 burst, 10/sec sustained
    sgcWSRateLimiter1.TokenBucket.Enabled := True;
    sgcWSRateLimiter1.TokenBucket.Capacity := 50;
    sgcWSRateLimiter1.TokenBucket.RefillRate := 10;
    sgcWSRateLimiter1.TokenBucket.RefillIntervalMs := 1000;

    // Per-IP sliding window: 60 requests per minute per IP
    sgcWSRateLimiter1.PerIP.Enabled := True;
    sgcWSRateLimiter1.PerIP.Strategy := rlsSliding;
    sgcWSRateLimiter1.PerIP.MaxRequests := 60;
    sgcWSRateLimiter1.PerIP.WindowSec := 60;

    // Per-endpoint: expensive report endpoint = 10 req/min
    sgcWSRateLimiter1.PerEndpoint.Enabled := True;
    with sgcWSRateLimiter1.PerEndpoint.Rules.Add as TsgcRateLimitRuleItem do
    begin
      Name := 'expensive-report';
      Pattern := '*/api/v1/expensive-report*';
      Strategy := rlsSliding;
      MaxRequests := 10;
      WindowSec := 60;
    end;

    // Burst protection: 50 requests in 500ms triggers 30s cooldown
    sgcWSRateLimiter1.BurstProtection.Enabled := True;
    sgcWSRateLimiter1.BurstProtection.BurstThreshold := 50;
    sgcWSRateLimiter1.BurstProtection.BurstWindowMs := 500;
    sgcWSRateLimiter1.BurstProtection.CooldownSec := 30;

    // Monthly quota per API key: 10000 requests/month
    sgcWSRateLimiter1.Quotas.Enabled := True;
    with sgcWSRateLimiter1.Quotas.Quotas.Add as TsgcRateLimitQuotaItem do
    begin
      Name := 'free-tier-monthly';
      Scope := qscPerKey;
      Period := qpMonth;
      Limit := 10000;
    end;

    // HTTP 429 response with Retry-After and X-RateLimit-* headers
    sgcWSRateLimiter1.Response.StatusCode := 429;
    sgcWSRateLimiter1.Response.Message := 'Too Many Requests';
    sgcWSRateLimiter1.Response.RetryAfterHeader := True;
    sgcWSRateLimiter1.Response.IncludeRateLimitHeaders := True;

    // Persist state across restarts
    sgcWSRateLimiter1.StorageFile := 'ratelimit.dat';
    sgcWSRateLimiter1.LoadStateFromFile('ratelimit.dat');

    // Assign to server
    sgcWebSocketHTTPServer1.RateLimiter := sgcWSRateLimiter1;

    // Handle throttled events
    procedure TForm1.RateLimiterThrottled(Sender: TObject;
      const aKey, aReason: string; var Allow: Boolean);
    begin
      Log('Throttled ' + aKey + ': ' + aReason);
    end;

    // Handle quota exhausted events
    procedure TForm1.RateLimiterQuotaExceeded(Sender: TObject;
      const aKey, aQuotaName: string);
    begin
      Log('Quota ' + aQuotaName + ' exceeded for ' + aKey);
    end;
    

 

Thread Safety

 

The rate limiter is fully thread-safe. All public methods use internal critical sections and thread-safe counters to protect concurrent access. The component can be shared by multiple server instances and accessed from any thread (server event handlers, timer threads, background workers) without external synchronization.

 

Reference