TsgcWSRateLimiter implements a full-featured rate limiting component that protects server endpoints from excessive traffic, abuse and scraping.
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.
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.
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.
| Property | Type | Default | Description |
| Enabled | Boolean | True | Master switch. When False, IsAllowed / Consume / IsConnectionAllowed / IsMessageAllowed always return True and no counters are updated. |
| StorageFile | string | ‘’ | Optional file path used by SaveStateToFile / LoadStateFromFile for counter persistence across restarts. |
sgcWSRateLimiter1.Enabled := True;
sgcWSRateLimiter1.StorageFile := 'ratelimit.dat';
sgcWSRateLimiter1.LoadStateFromFile(sgcWSRateLimiter1.StorageFile);
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.
| Property | Type | Default | Description |
| Enabled | Boolean | False | Enables the Token Bucket algorithm as the global default strategy. |
| Capacity | Integer | 100 | Maximum number of tokens the bucket can hold. Controls the maximum burst size. |
| RefillRate | Integer | 10 | Number of tokens added to the bucket every RefillIntervalMs. |
| RefillIntervalMs | Integer | 1000 | Refill 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
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.
| Property | Type | Default | Description |
| Enabled | Boolean | False | Enables the Sliding Window algorithm as the global default strategy. |
| WindowSec | Integer | 60 | Size of the rolling window in seconds. |
| MaxRequests | Integer | 1000 | Maximum 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
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.
| Property | Type | Default | Description |
| Enabled | Boolean | False | Enables the Fixed Window algorithm as the global default strategy. |
| WindowSec | Integer | 60 | Size of the fixed window in seconds. The counter resets to 0 when the window elapses. |
| MaxRequests | Integer | 1000 | Maximum 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;
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.
| Property | Type | Default | Description |
| Enabled | Boolean | False | Enables per-IP rate limiting. |
| MaxRequests | Integer | 100 | Maximum requests per window (or bucket capacity when Strategy is rlsTokenBucket). |
| WindowSec | Integer | 60 | Window size in seconds. For Token Bucket also used to derive the refill rate (MaxRequests / WindowSec tokens per second). |
| Strategy | TsgcRateLimitStrategy | rlsTokenBucket | Algorithm 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;
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.
| Property | Type | Default | Description |
| Enabled | Boolean | False | Enables per-API-key rate limiting. |
| MaxRequests | Integer | 100 | Maximum requests per window (or bucket capacity for Token Bucket). |
| WindowSec | Integer | 60 | Window size in seconds. |
| Strategy | TsgcRateLimitStrategy | rlsTokenBucket | Algorithm 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
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.
| Property | Type | Default | Description |
| Enabled | Boolean | False | Enables per-user rate limiting. |
| MaxRequests | Integer | 100 | Maximum requests per window (or bucket capacity for Token Bucket). |
| WindowSec | Integer | 60 | Window size in seconds. |
| Strategy | TsgcRateLimitStrategy | rlsTokenBucket | Algorithm 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
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.
| Property | Type | Default | Description |
| Enabled | Boolean | False | Enables the PerEndpoint rule set. When False no items are evaluated. |
| Rules | TCollection | empty | Collection of TsgcRateLimitRuleItem. Order matters — first match wins. |
Each TsgcRateLimitRuleItem exposes:
| Property | Type | Default | Description |
| Name | string | ‘’ | Friendly name shown in the object inspector. |
| Enabled | Boolean | True | Disables this item without deleting it. |
| Pattern | string | ‘’ | Wildcard pattern matched against the key (case-insensitive). Supports * and ?. |
| MaxRequests | Integer | 100 | Maximum requests per window / bucket capacity. |
| WindowSec | Integer | 60 | Window size in seconds. |
| Strategy | TsgcRateLimitStrategy | rlsTokenBucket | Algorithm 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;
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.
| Property | Type | Default | Description |
| Enabled | Boolean | False | Enables the quota rule set. When False no items are evaluated. |
| Quotas | TCollection | empty | Collection of TsgcRateLimitQuotaItem. All enabled items are evaluated in order. |
Each TsgcRateLimitQuotaItem exposes:
| Property | Type | Default | Description |
| Name | string | ‘’ | Friendly name reported in OnQuotaExceeded and in Result.Reason ("quota:Name"). |
| Enabled | Boolean | True | Disables this quota without deleting it. |
| Scope | TsgcRateLimitQuotaScope | qscPerKey | qscPerKey counts independently per key; qscGlobal counts across all keys combined. |
| Period | TsgcRateLimitQuotaPeriod | qpHour | qpHour, qpDay or qpMonth. Counter resets automatically at the start of each period. |
| Limit | Int64 | 10000 | Maximum 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;
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.
| Property | Type | Default | Description |
| Enabled | Boolean | False | Enables the burst detector. |
| BurstThreshold | Integer | 50 | Number of requests within BurstWindowMs that triggers a cooldown. |
| BurstWindowMs | Integer | 500 | Burst observation window in milliseconds. |
| CooldownSec | Integer | 30 | Duration 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;
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.
| Property | Type | Default | Description |
| StatusCode | Integer | 429 | HTTP status code returned for throttled requests. Use 429 (standard) or 503 for maintenance-style rejection. |
| Message | string | "Too Many Requests" | Body of the HTTP response for throttled requests. |
| RetryAfterHeader | Boolean | True | When True adds a Retry-After header with the computed retry delay in seconds. |
| IncludeRateLimitHeaders | Boolean | True | When 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;
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.
| Property | Type | Description |
| TotalRequests | Int64 | Total number of requests evaluated since the last Reset. |
| TotalThrottled | Int64 | Total number of requests rejected by the strategy or burst protection. |
| TotalQuotaExceeded | Int64 | Total number of requests rejected because a quota was reached. |
| ActiveKeys | Integer | Number of keys currently tracked (buckets + window counters + active connections). |
| Uptime | Int64 | Seconds 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;
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.
| Method | Description |
| 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. |
| ResetAll | Clears 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. |
When assigned to a server's RateLimiter property, the component automatically:
// 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;
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.