API Index — Namespaces, Packages, Reports, Indices
RateLimitMiddleware
in package
implements
MiddlewareInterface
Rate-Limit Middleware (sliding window, per-IP, shared across workers)
Tracks request counts per client IP in a shared Store table. When a
single IP exceeds $limit requests inside $window seconds, further
requests receive a configurable HTTP error (default 429 Too Many Requests)
with a Retry-After header.
nginx parity
limit_req_zone $binary_remote_addr zone=one:10m rate=60r/m;
limit_req zone=one burst=5 nodelay;
ZealPHP equivalent:
$app->addMiddleware(new RateLimitMiddleware(
limit: 60,
window: 60,
burst: 5,
nodelay: true,
tableName: 'rate_limit',
));
Algorithm note — fixed window vs leaky bucket
nginx's limit_req uses a leaky-bucket (token-drain) algorithm with
millisecond precision. ZealPHP uses a fixed window that resets every
$window seconds. The practical difference is the "thundering-herd at
boundary" problem: a fixed window allows up to $limit requests at the
tail of one window and another $limit at the head of the next window —
a worst-case 2× burst. A leaky bucket smooths this out. For spam/abuse
defence the fixed window is sufficient; for billing-grade fairness or tight
concurrency control, consider implementing a leaky-bucket variant.
burst= / nodelay= / delay=
burst=N— allow up to N extra requests above thelimitper window before rejecting. Requests within the burst quota are forwarded.nodelay=true— burst requests are forwarded immediately (no artificial delay). This matches nginxlimit_req ... nodelay.- Without burst (default
burst=0), any request over the limit is rejected.
Trusted-proxy / X-Forwarded-For integration (B2 fix)
The zone key is now resolved via App::clientIp() which honours
App::$trusted_proxies. Operators deploying behind Traefik/nginx must
configure App::trustedProxies() so the rate-limit key is the real client
IP rather than the proxy's IP.
Store-full failure policy (B10 fix)
When the OpenSwoole Table is full, Store::set() returns false. The
middleware detects this, logs a warning via elog(), and fails open
(passes the request through). This is an explicit policy choice: rejecting
an unknown IP because the table is full would be overly aggressive. Operators
should size their table generously and monitor the log for Store table full
warnings.
Dry-run mode
dryRun=true runs all accounting and logs what would have been blocked, but
forwards every request regardless. Use this to calibrate rate-limit settings
on production traffic without impacting availability.
Configurable reject status
rejectStatus defaults to 429. Pass 503 for nginx parity (nginx historically
uses 503 for rate-limit rejections). Any 4xx/5xx code is accepted.
Loopback bypass
By default, requests from 127.0.0.1 / ::1 are not rate-limited so the
integration test suite can run repeatedly without php app.php restart. Set
ZEALPHP_RATE_LIMIT_LOOPBACK=1 to opt in (useful when testing the limiter).
Store table schema (create before $app->run())
Store::make('rate_limit', 16384, [
'ip' => [\OpenSwoole\Table::TYPE_STRING, 64],
'count' => [\OpenSwoole\Table::TYPE_INT, 4],
'reset' => [\OpenSwoole\Table::TYPE_INT, 4],
]);
$app->addMiddleware(new \ZealPHP\Middleware\RateLimitMiddleware(
limit: 60,
window: 60,
tableName: 'rate_limit',
));
If the table doesn't exist when the request arrives the middleware
fails-open (passes the request through) and logs once via elog().
Table of Contents
Interfaces
- MiddlewareInterface
Properties
- $burst : int
- $dryRun : bool
- $limit : int
- $nodelay : bool
- $rejectStatus : int
- $tableName : string
- $warnedMissingTable : bool
- $window : int
Methods
- __construct() : mixed
- process() : ResponseInterface
- isLoopback() : bool
- logDryRunBlock() : void
- tooMany() : ResponseInterface
Properties
$burst
private
int
$burst
= 0
$dryRun
private
bool
$dryRun
= false
$limit
private
int
$limit
= 60
$nodelay
private
bool
$nodelay
= false
$rejectStatus
private
int
$rejectStatus
= 429
$tableName
private
string
$tableName
= 'rate_limit'
$warnedMissingTable
private
static bool
$warnedMissingTable
= false
$window
private
int
$window
= 60
Methods
__construct()
public
__construct([int $limit = 60 ][, int $window = 60 ][, string $tableName = 'rate_limit' ][, int $burst = 0 ][, bool $nodelay = false ][, int $rejectStatus = 429 ][, bool $dryRun = false ]) : mixed
Parameters
- $limit : int = 60
- $window : int = 60
- $tableName : string = 'rate_limit'
- $burst : int = 0
- $nodelay : bool = false
- $rejectStatus : int = 429
- $dryRun : bool = false
process()
public
process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
Parameters
- $request : ServerRequestInterface
- $handler : RequestHandlerInterface
Return values
ResponseInterfaceisLoopback()
private
isLoopback(string $ip) : bool
Parameters
- $ip : string
Return values
boollogDryRunBlock()
private
logDryRunBlock(string $ip, int $count, int $retryAfterSeconds) : void
Parameters
- $ip : string
- $count : int
- $retryAfterSeconds : int
tooMany()
private
tooMany(int $retryAfterSeconds) : ResponseInterface
Parameters
- $retryAfterSeconds : int