snipershady/ratelimiter

A free and easy-to-use rate limiter

Maintainers

Package info

github.com/snipershady/ratelimiter

Homepage

pkg:composer/snipershady/ratelimiter

Statistics

Installs: 3 461

Dependents: 0

Suggesters: 0

Stars: 4

Open Issues: 0

1.0.8 2026-06-12 12:13 UTC

This package is auto-updated.

Last update: 2026-06-12 12:16:04 UTC


README

PHP Version License Packagist

A free and easy-to-use rate limiter for PHP applications.

Context

You need to limit network traffic access to a specific function in a specific timeframe. Rate limiting may help to stop some kinds of malicious activity such as brute force attacks, DDoS, and API abuse.

Installation

composer require snipershady/ratelimiter

Requirements

Composer packages

Package Version Notes
PHP ^8.3 minimum version
predis/predis ^3.2 required only for CacheEnum::REDIS

System extensions

Native PHP extensions are not managed by Composer. Install only the ones needed by the backends you use.

Extension Required by
ext-apcu CacheEnum::APCU
ext-redis CacheEnum::PHP_REDIS
ext-memcached CacheEnum::MEMCACHED

Debian / Ubuntu

PHP_VER=$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;')

# APCu
apt-get install php${PHP_VER}-apcu

# Redis (php-redis native extension)
apt-get install php${PHP_VER}-redis

# Memcached (php-memcached native extension — note the 'd')
apt-get install php${PHP_VER}-memcached

CLI Usage

For CLI usage, remember to enable APCu in your php.ini:

apc.enable_cli=1

Available Cache Backends

Backend Enum Description
APCu CacheEnum::APCU Local in-memory cache, no external server required
Predis CacheEnum::REDIS Redis via Predis library (pure PHP)
PhpRedis CacheEnum::PHP_REDIS Redis via php-redis native extension (better performance)
Memcached CacheEnum::MEMCACHED Memcached via php-memcached native extension

API Reference

isLimited(string $key, int $limit, int $ttl): bool

Check if a key has exceeded the rate limit.

Parameter Type Description
$key string Unique identifier for the rate limit (e.g., __METHOD__)
$limit int Maximum number of attempts allowed
$ttl int Time window in seconds

Returns: true if the limit has been exceeded, false otherwise.

isLimitedWithBan(string $key, int $limit, int $ttl, int $maxAttempts, int $banTimeFrame, int $banTtl, ?string $clientIp): bool

Check if a key has exceeded the rate limit, with progressive ban support for repeat offenders. Each violation (a request that exceeds $limit within $ttl) increments a per-client counter. When that counter reaches $maxAttempts within the $banTimeFrame observation window, the client is banned: its next time window is extended to $banTtl instead of the normal $ttl.

Parameter Type Description
$key string Unique identifier for the rate limit
$limit int Maximum number of requests allowed in $ttl seconds
$ttl int Normal time window in seconds
$maxAttempts int Number of violations allowed before a ban is applied
$banTimeFrame int Observation window in seconds during which violations are counted. The violation counter resets after $banTimeFrame seconds from the first violation, regardless of subsequent activity (fixed window).
$banTtl int Extended time window in seconds applied when the client is banned ($banTtl replaces $ttl for the duration of the ban)
$clientIp string|null When provided, each IP address maintains its own independent violation counter. Pass null to apply a shared global counter for the key.

Returns: true if the limit has been exceeded, false otherwise.

How the three time parameters interact

$ttl          Normal window: max $limit requests every $ttl seconds
$banTimeFrame Observation window: counts how many times the limit was
              exceeded. Resets $banTimeFrame seconds after the first violation.
$banTtl       Punishment window: replaces $ttl when the client has exceeded
              the limit $maxAttempts times within $banTimeFrame seconds.

Concrete timeline$limit=1, $ttl=5s, $maxAttempts=2, $banTimeFrame=30s, $banTtl=120s:

 t=0s   Request 1: allowed  (counter=1, within limit)
 t=1s   Request 2: BLOCKED  → violation #1 recorded, violation TTL=30s starts
 t=6s   Normal window ($ttl=5s) expired
 t=6s   Request 3: allowed  (new window, violation_count=1 < maxAttempts=2)
 t=7s   Request 4: BLOCKED  → violation #2 recorded  ← ban threshold reached!
        violation_count=2 expires at t≈30s (banTimeFrame from t≈1s)
 t=12s  Normal window expired
 t=12s  Request 5: allowed  (new window; but violation_count=2 ≥ maxAttempts
                              → window is extended: this key now lives 120s)
 t=13s  Request 6: BLOCKED  (inside the 120s ban window)
 ...    All requests blocked until t≈132s (t=12 + banTtl=120)
 t=31s  Violation counter expired (banTimeFrame=30s from t≈1s)
 t=132s Ban window ($banTtl=120s) expired
 t=132s Request N: allowed  (violation_count=0, normal $ttl=5s applies again)

clearRateLimitedKey(string $key): bool

Remove a rate limit key, resetting its counter.

Parameter Type Description
$key string The key to clear

Returns: true on success, false on failure.

Usage Examples

Common imports

use Predis\Client;
use RateLimiter\Enum\CacheEnum;
use RateLimiter\Service\AbstractRateLimiterService;

APCu

No external server required. Ideal for single-server deployments or CLI tools.

$limiter = AbstractRateLimiterService::factory(CacheEnum::APCU);
$key     = __METHOD__;
$limit   = 2;
$ttl     = 3;

if ($limiter->isLimited($key, $limit, $ttl)) {
    throw new \Exception("LIMIT REACHED: YOU SHALL NOT PASS!");
}

Redis — Predis

Pure-PHP Redis client; no native extension required.

$redis = new Client([
    'scheme'     => 'tcp',
    'host'       => '192.168.0.100',
    'port'       => 6379,
    'persistent' => true,
]);

$limiter = AbstractRateLimiterService::factory(CacheEnum::REDIS, $redis);
$key     = __METHOD__;
$limit   = 2;
$ttl     = 3;

if ($limiter->isLimited($key, $limit, $ttl)) {
    throw new \Exception("LIMIT REACHED: YOU SHALL NOT PASS!");
}

Redis — PhpRedis

Native ext-redis extension; better raw performance than Predis.

$redis = new \Redis();
$redis->pconnect(
    '192.168.0.100',
    6379,
    2,
    'persistent_id_rl'
);

$limiter = AbstractRateLimiterService::factory(CacheEnum::PHP_REDIS, $redis);
$key     = __METHOD__;
$limit   = 2;
$ttl     = 3;

if ($limiter->isLimited($key, $limit, $ttl)) {
    throw new \Exception("LIMIT REACHED: YOU SHALL NOT PASS!");
}

Memcached

Requires ext-memcached. Passing a persistent_id reuses the connection pool across requests; the getServerList() guard prevents registering the same server twice.

$memcached = new \Memcached('persistent_id_rl');
if (!$memcached->getServerList()) {
    $memcached->addServer('192.168.0.100', 11211);
}

$limiter = AbstractRateLimiterService::factory(CacheEnum::MEMCACHED, $memcached);
$key     = __METHOD__;
$limit   = 2;
$ttl     = 3;

if ($limiter->isLimited($key, $limit, $ttl)) {
    throw new \Exception("LIMIT REACHED: YOU SHALL NOT PASS!");
}

Rate Limit with Ban

Use isLimitedWithBan when you want to progressively punish repeat offenders with longer block windows. The only difference between backends is the factory call — the parameters and behaviour are identical.

With APCu

$limiter = AbstractRateLimiterService::factory(CacheEnum::APCU);

$key          = __METHOD__;
$limit        = 5;
$ttl          = 60;
$maxAttempts  = 3;
$banTimeFrame = 300;
$banTtl       = 3600;
$clientIp     = $_SERVER['REMOTE_ADDR'] ?? null;

if ($limiter->isLimitedWithBan($key, $limit, $ttl, $maxAttempts, $banTimeFrame, $banTtl, $clientIp)) {
    throw new \RuntimeException("Too many login attempts. Please try again later.");
}

With Predis

$redis = new Client([
    'scheme'     => 'tcp',
    'host'       => '192.168.0.100',
    'port'       => 6379,
    'persistent' => true,
]);

$limiter = AbstractRateLimiterService::factory(CacheEnum::REDIS, $redis);

$key          = __METHOD__;
$limit        = 5;
$ttl          = 60;
$maxAttempts  = 3;
$banTimeFrame = 300;
$banTtl       = 3600;
$clientIp     = $_SERVER['REMOTE_ADDR'] ?? null;

if ($limiter->isLimitedWithBan($key, $limit, $ttl, $maxAttempts, $banTimeFrame, $banTtl, $clientIp)) {
    throw new \RuntimeException("Too many login attempts. Please try again later.");
}

With PhpRedis

$redis = new \Redis();
$redis->pconnect('192.168.0.100', 6379, 2, 'persistent_id_rl');

$limiter = AbstractRateLimiterService::factory(CacheEnum::PHP_REDIS, $redis);

$key          = __METHOD__;
$limit        = 5;
$ttl          = 60;
$maxAttempts  = 3;
$banTimeFrame = 300;
$banTtl       = 3600;
$clientIp     = $_SERVER['REMOTE_ADDR'] ?? null;

if ($limiter->isLimitedWithBan($key, $limit, $ttl, $maxAttempts, $banTimeFrame, $banTtl, $clientIp)) {
    throw new \RuntimeException("Too many login attempts. Please try again later.");
}

With Memcached

$memcached = new \Memcached('persistent_id_rl');
if (!$memcached->getServerList()) {
    $memcached->addServer('192.168.0.100', 11211);
}

$limiter = AbstractRateLimiterService::factory(CacheEnum::MEMCACHED, $memcached);

$key          = __METHOD__;
$limit        = 5;
$ttl          = 60;
$maxAttempts  = 3;
$banTimeFrame = 300;
$banTtl       = 3600;
$clientIp     = $_SERVER['REMOTE_ADDR'] ?? null;

if ($limiter->isLimitedWithBan($key, $limit, $ttl, $maxAttempts, $banTimeFrame, $banTtl, $clientIp)) {
    throw new \RuntimeException("Too many login attempts. Please try again later.");
}

Understanding $banTimeFrame

$banTimeFrame is the observation window that determines how long a violation is "remembered". It answers the question: "How many times has this client exceeded the limit in the last N seconds?".

$ttl          → How long each rate-limit window lasts (normal behaviour)
$banTimeFrame → How long violations are tracked (observation window)
$banTtl       → How long a ban lasts once the client is flagged

The violation counter is a fixed window starting at the first violation:

  • It does not reset on each new violation (no sliding window).
  • After $banTimeFrame seconds it expires and the client is "forgiven".

Visual example$limit=5, $ttl=60s, $maxAttempts=3, $banTimeFrame=300s, $banTtl=3600s:

 t=0s     6 rapid requests → 5 allowed, 1 BLOCKED  → violation #1 (counter TTL = 300s)
 t=60s    Window resets. 6 requests again           → violation #2
 t=120s   Window resets. 6 requests again           → violation #3  ← ban threshold!
           violation_count = 3 >= maxAttempts=3
 t=180s   Window resets. Client tries again:
           violation_count still alive (expires at t≈300s)
           → ban applied: new window is 3600s instead of 60s
           → client blocked for 1 hour
 t=300s   Violation counter expires (banTimeFrame elapsed from t=0)
 t=3780s  Ban window expires (t=180 + banTtl=3600)
 t=3780s  Client can try again with a fresh violation counter

Per-client isolation with $clientIp

When $clientIp is provided, each IP address has its own independent violation counter. Banning 192.168.1.1 has no effect on 192.168.1.2:

// Client A: banned after 3 violations
$limiter->isLimitedWithBan($key, $limit, $ttl, $maxAttempts, $banTimeFrame, $banTtl, '192.168.1.1');

// Client B: unaffected, starts from zero violations
$limiter->isLimitedWithBan($key, $limit, $ttl, $maxAttempts, $banTimeFrame, $banTtl, '192.168.1.2');

Pass null to use a shared global counter for the key (all clients contribute to the same violation count — useful when you want to protect a resource globally regardless of origin).

Clearing a Rate Limit Key

clearRateLimitedKey resets the counter for a given key immediately. Useful after a successful authentication, a manual unban, or during testing.

// Works identically for every backend — swap the factory call as needed.
$limiter = AbstractRateLimiterService::factory(CacheEnum::APCU);

$key = 'App\Controller\LoginController::login';

if ($limiter->clearRateLimitedKey($key)) {
    // Counter reset; the next request will be treated as the first in a new window.
}

When using isLimitedWithBan, only the request counter is cleared by this method. The violation counter (used to track bans) lives under a separate internal key and is managed automatically by the library.

Development

Dev dependencies

Package Version Purpose
phpunit/phpunit ^12.1 test runner
phpstan/phpstan ^2.1 static analysis
friendsofphp/php-cs-fixer ^3.90 code style
rector/rector ^2.1 automated refactoring

Available Scripts

Command Description
composer test Run PHPUnit tests
composer phpstan Run PHPStan static analysis
composer cs-fix Fix code style with PHP-CS-Fixer
composer cs-check Check code style (dry-run)
composer rector Run Rector refactoring
composer rector-dry Preview Rector changes
composer quality Run all quality tools (Rector + CS-Fixer)
composer quality-check Check quality without changes

License

This project is licensed under the GPL-3.0-or-later License - see the LICENSE file for details.

Author

Stefano Perrini - spinfo.it