Alpha ZealPHP is early-stage and under active development. APIs may change between minor versions until v1.0. Feedback and bug reports welcome on GitHub.
API Index — Namespaces, Packages, Reports, Indices

ConcurrencyLimitMiddleware
in package
implements MiddlewareInterface

Concurrency-Limit Middleware

Bounds the number of in-flight requests. Supports two modes:

Global mode (backward-compatible, original behaviour) A single shared Counter (OpenSwoole\Atomic) caps total in-flight requests across all clients. Simple, zero-allocation, but one slow client can exhaust the global cap and 503 everyone else.

Per-key mode (nginx limit_conn parity) A Store table (OpenSwoole\Table) tracks in-flight counts per key. The default key is the client IP (proxy-aware via App::clientIp()), matching nginx $binary_remote_addr. A custom key resolver can be supplied for other partitioning schemes (e.g. API key, tenant ID).

nginx equivalent: limit_conn_zone $binary_remote_addr zone=addr:10m; limit_conn addr 100; limit_conn_status 503; limit_conn_dry_run off;

The counter/store entry is incremented on entry and decremented on exit via try/finally, so handlers that throw still decrement correctly.

Pre-fork footgun — both the Counter and the Store table MUST be instantiated/created BEFORE $app->run() so all forked workers share the same shared-memory segment. Creating them after run() gives each worker its own isolated counter — the limit will be maxConcurrent × numWorkers in practice, with no cross-worker enforcement.

Usage — global mode (existing API, no change):

$inflight = new \ZealPHP\Counter();
$app->addMiddleware(new \ZealPHP\Middleware\ConcurrencyLimitMiddleware(
    maxConcurrent: 100,
    counter:       $inflight,
));

Usage — per-key mode (nginx limit_conn parity):

Store::make('conn_limit', 4096, [
    'count' => [\OpenSwoole\Table::TYPE_INT, 4],
]);

$app->addMiddleware(new \ZealPHP\Middleware\ConcurrencyLimitMiddleware(
    maxConcurrent: 20,
    counter:       null,          // no global counter
    tableName:     'conn_limit',  // per-key Store table
));

Usage — per-key + global cap together:

$global = new \ZealPHP\Counter();
$app->addMiddleware(new \ZealPHP\Middleware\ConcurrencyLimitMiddleware(
    maxConcurrent: 20,     // per-key cap
    counter:       $global, // also enforce global cap (separate Counter)
    tableName:     'conn_limit',
    globalMax:     500,    // global ceiling across all clients
));

Options: rejectStatus int HTTP status on rejection (400599, default 503) dryRun bool Observe + log, never enforce (default false) keyResolver callable|null fn(ServerRequestInterface): string — override key; default uses App::clientIp() globalMax int When > 0 and a $counter is provided, also enforce this global ceiling alongside the per-key limit

Table of Contents

Interfaces

MiddlewareInterface

Properties

$counter  : Counter|null
$dryRun  : bool
$globalMax  : int
$keyResolver  : callable(ServerRequestInterface): string
$maxConcurrent  : int
$rejectStatus  : int
$tableName  : string|null
$warnedMissingTable  : bool

Methods

__construct()  : mixed
process()  : ResponseInterface
processGlobal()  : ResponseInterface
processPerKey()  : ResponseInterface
reject()  : ResponseInterface

Properties

Methods

__construct()

public __construct(int $maxConcurrent[, Counter|null $counter = null ][, string|null $tableName = null ][, int $globalMax = 0 ][, int $rejectStatus = 503 ][, bool $dryRun = false ][, callable(ServerRequestInterface): string|null $keyResolver = null ]) : mixed
Parameters
$maxConcurrent : int

Per-key (or global) in-flight cap.

$counter : Counter|null = null

Optional global Counter (global mode when $tableName is null; optional additional global cap in per-key mode).

$tableName : string|null = null

Store table for per-key limiting. Must be created before $app->run().

$globalMax : int = 0

Global cap enforced via $counter when

0 and $tableName is set. Ignored when $tableName is null (legacy mode uses $maxConcurrent as the global cap).

$rejectStatus : int = 503

HTTP status on rejection (400599).

$dryRun : bool = false

Log rejections without enforcing.

$keyResolver : callable(ServerRequestInterface): string|null = null

Override key; default = clientIp().

process()

public process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
Parameters
$request : ServerRequestInterface
$handler : RequestHandlerInterface
Return values
ResponseInterface

processGlobal()

private processGlobal(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
Parameters
$request : ServerRequestInterface
$handler : RequestHandlerInterface
Return values
ResponseInterface

processPerKey()

private processPerKey(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
Parameters
$request : ServerRequestInterface
$handler : RequestHandlerInterface
Return values
ResponseInterface
On this page