PHPackages                             monkeyscloud/monkeyslegion-rate-limit - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Caching](/categories/caching)
4. /
5. monkeyscloud/monkeyslegion-rate-limit

ActiveLibrary[Caching](/categories/caching)

monkeyscloud/monkeyslegion-rate-limit
=====================================

Production-grade rate limiting with token bucket and sliding window algorithms, Redis backend, per-route + per-user + per-IP composite buckets, and PSR-15 middleware for MonkeysLegion.

1.0.0(2w ago)00MITPHPPHP ^8.4

Since May 23Pushed 2w agoCompare

[ Source](https://github.com/MonkeysCloud/MonkeysLegion-Rate-Limit)[ Packagist](https://packagist.org/packages/monkeyscloud/monkeyslegion-rate-limit)[ Docs](https://monkeyslegion.com)[ RSS](/packages/monkeyscloud-monkeyslegion-rate-limit/feed)WikiDiscussions main Synced 1w ago

READMEChangelogDependencies (7)Versions (2)Used By (0)

MonkeysLegion Rate Limit
========================

[](#monkeyslegion-rate-limit)

Production-grade rate limiting for MonkeysLegion Framework with token bucket and sliding window algorithms, Redis backend, and PSR-15 middleware.

Features
--------

[](#features)

- **Two algorithms**: Token Bucket (burst-friendly) and Sliding Window (precise counting)
- **Redis Lua scripts**: Atomic operations — no race conditions at scale
- **Composite key resolvers**: Per-IP, per-user, per-route, or any combination (`ip+route`, `user+route`)
- **`#[RateLimit]` attribute**: Repeatable, class + method targets, named limiter references
- **PSR-15 middleware**: RFC-compliant rate limit headers
- **Resilient storage**: Fail-open/fail-closed modes with circuit breaker — **no 500 errors when Redis is down**
- **MLC configuration**: Full config file with env var overrides and named limiter definitions
- **Telemetry integration**: Optional metrics for monitoring (counters, gauges)
- **GDPR compliance**: Optional IP hashing, trusted proxy support
- **PHP 8.4**: Property hooks, backed enums, asymmetric visibility

Installation
------------

[](#installation)

```
composer require monkeyscloud/monkeyslegion-rate-limit
```

For production use with Redis:

```
# Ensure ext-redis is installed
pecl install redis
```

---

MLC Configuration
-----------------

[](#mlc-configuration)

Copy the config file to your project's `config/` directory:

```
cp vendor/monkeyscloud/monkeyslegion-rate-limit/config/rate-limit.mlc config/rate-limit.mlc
```

### Full Configuration Reference

[](#full-configuration-reference)

```
rate_limit {
    #
    #--------------------------------------------------------------------------
    # Global Defaults
    #--------------------------------------------------------------------------
    #
    max_attempts   = ${RATE_LIMIT_MAX_ATTEMPTS:-60}
    window_seconds = ${RATE_LIMIT_WINDOW_SECONDS:-60}
    algorithm      = ${RATE_LIMIT_ALGORITHM:-token_bucket}
    by             = ${RATE_LIMIT_BY:-ip}
    cost           = ${RATE_LIMIT_COST:-1}

    #
    #--------------------------------------------------------------------------
    # Storage Backend
    #--------------------------------------------------------------------------
    #
    # Supported drivers: "redis", "memory"
    #
    storage {
        driver = ${RATE_LIMIT_STORAGE:-redis}

        redis {
            host     = ${REDIS_HOST:-127.0.0.1}
            port     = ${REDIS_PORT:-6379}
            username = ${REDIS_USERNAME:-null}
            password = ${REDIS_PASSWORD:-null}
            database = ${REDIS_RATE_LIMIT_DB:-1}
            timeout  = ${REDIS_TIMEOUT:-2.0}
            prefix   = ${RATE_LIMIT_KEY_PREFIX:-ml_rl:}
        }
    }

    #
    #--------------------------------------------------------------------------
    # Resilience — Fail-Open / Circuit Breaker
    #--------------------------------------------------------------------------
    #
    resilience {
        enabled                  = ${RATE_LIMIT_RESILIENCE:-true}
        fail_open                = ${RATE_LIMIT_FAIL_OPEN:-true}
        fallback_driver          = ${RATE_LIMIT_FALLBACK:-memory}
        circuit_threshold        = ${RATE_LIMIT_CIRCUIT_THRESHOLD:-3}
        circuit_recovery_seconds = ${RATE_LIMIT_CIRCUIT_RECOVERY:-30}
    }

    #
    #--------------------------------------------------------------------------
    # IP Resolution
    #--------------------------------------------------------------------------
    #
    ip {
        trusted_proxies = ${RATE_LIMIT_TRUSTED_PROXIES:-null}
        hash_ips        = ${RATE_LIMIT_HASH_IPS:-false}
    }

    #
    #--------------------------------------------------------------------------
    # Named Limiters
    #--------------------------------------------------------------------------
    #
    limiters {
        api {
            max_attempts   = ${RATE_LIMIT_API_MAX:-100}
            window_seconds = ${RATE_LIMIT_API_WINDOW:-60}
            algorithm      = token_bucket
            by             = ip
        }

        auth {
            max_attempts   = ${RATE_LIMIT_AUTH_MAX:-5}
            window_seconds = ${RATE_LIMIT_AUTH_WINDOW:-300}
            algorithm      = sliding_window
            by             = ip
        }

        uploads {
            max_attempts   = ${RATE_LIMIT_UPLOADS_MAX:-10}
            window_seconds = ${RATE_LIMIT_UPLOADS_WINDOW:-3600}
            algorithm      = token_bucket
            by             = user+route
            cost           = 5
        }
    }

    #
    #--------------------------------------------------------------------------
    # Telemetry
    #--------------------------------------------------------------------------
    #
    telemetry {
        enabled = ${RATE_LIMIT_TELEMETRY:-true}
    }

    #
    #--------------------------------------------------------------------------
    # Response Customization
    #--------------------------------------------------------------------------
    #
    response {
        message         = ${RATE_LIMIT_RESPONSE_MESSAGE:-Too many requests. Please try again later.}
        include_headers = ${RATE_LIMIT_INCLUDE_HEADERS:-true}
    }
}

```

### Environment Variable Quick Reference

[](#environment-variable-quick-reference)

VariableDefaultDescription`RATE_LIMIT_STORAGE``redis`Storage driver: `redis` or `memory``RATE_LIMIT_MAX_ATTEMPTS``60`Default requests per window`RATE_LIMIT_WINDOW_SECONDS``60`Default window in seconds`RATE_LIMIT_ALGORITHM``token_bucket`Default algorithm`RATE_LIMIT_BY``ip`Default key strategy`RATE_LIMIT_FAIL_OPEN``true`Allow traffic when Redis is down`RATE_LIMIT_CIRCUIT_THRESHOLD``3`Failures before circuit opens`RATE_LIMIT_CIRCUIT_RECOVERY``30`Seconds before retrying Redis`REDIS_HOST``127.0.0.1`Redis server host`REDIS_PORT``6379`Redis server port`REDIS_RATE_LIMIT_DB``1`Redis database for rate limit keys`RATE_LIMIT_HASH_IPS``false`Hash IPs for GDPR compliance`RATE_LIMIT_TELEMETRY``true`Enable telemetry counters---

Bootstrapping with the Provider
-------------------------------

[](#bootstrapping-with-the-provider)

### Automatic (Recommended)

[](#automatic-recommended)

The `RateLimitProvider` wires everything from MLC config in a single call:

```
use MonkeysLegion\Mlc\Loader;
use MonkeysLegion\RateLimit\Provider\RateLimitProvider;

// 1. Load the MLC config
$config = $loader->loadOne('rate-limit');

// 2. Bootstrap the entire rate limit stack
$services = RateLimitProvider::register(
    config: $config->get('rate_limit', []),
    logger: $logger,                        // PSR-3 logger
);

// 3. Register the middleware in the router
$router->registerMiddleware(
    'rate-limit',
    $services['middleware'],
    priority: 100,
);
$router->addGlobalMiddleware('rate-limit');
```

The Provider creates:

- `$services['storage']` — The storage backend (Redis → ResilientStorage wrapper)
- `$services['manager']` — `RateLimiterManager` with named limiters from config
- `$services['middleware']` — `RateLimitMiddleware` ready for the router

### DI Container Registration

[](#di-container-registration)

```
// In your container definitions (e.g., di/definitions.php)
use MonkeysLegion\RateLimit\Provider\RateLimitProvider;
use MonkeysLegion\RateLimit\RateLimiterManager;
use MonkeysLegion\RateLimit\Middleware\RateLimitMiddleware;

return [
    // Load config
    'rate_limit.config' => fn(Config $config) => $config->get('rate_limit', []),

    // Bootstrap services
    'rate_limit.services' => fn(array $config, LoggerInterface $logger) =>
        RateLimitProvider::register(config: $config, logger: $logger),

    // Individual service bindings
    RateLimiterManager::class  => fn(array $services) => $services['manager'],
    RateLimitMiddleware::class => fn(array $services) => $services['middleware'],
];
```

### With Pre-existing Redis Connection

[](#with-pre-existing-redis-connection)

If your app already has a shared Redis connection (e.g., from the queue or cache package):

```
$services = RateLimitProvider::register(
    config: $config->get('rate_limit', []),
    logger: $logger,
    redis:  $existingRedis,  // Reuse connection — config host/port are ignored
);
```

### Manual Bootstrap (Without Provider)

[](#manual-bootstrap-without-provider)

```
use MonkeysLegion\RateLimit\RateLimiterManager;
use MonkeysLegion\RateLimit\Middleware\RateLimitMiddleware;
use MonkeysLegion\RateLimit\Storage\RedisStorage;
use MonkeysLegion\RateLimit\Storage\ResilientStorage;
use MonkeysLegion\RateLimit\Storage\InMemoryStorage;
use MonkeysLegion\RateLimit\RateLimiter;

// 1. Create Redis connection
$redis = new \Redis();
$redis->connect('127.0.0.1', 6379);
$redis->select(1);

// 2. Wrap with resilience
$storage = new ResilientStorage(
    primary:  new RedisStorage($redis, prefix: 'ml_rl:'),
    failOpen: true,
    fallback: new InMemoryStorage(),
    logger:   $logger,
);

// 3. Create manager and register named limiters
$manager = new RateLimiterManager($storage);

$manager->define('api', fn() => [
    'maxAttempts'   => 100,
    'windowSeconds' => 60,
    'by'            => 'ip',
    'algorithm'     => 'token_bucket',
]);

$manager->define('auth', fn() => [
    'maxAttempts'   => 5,
    'windowSeconds' => 300,
    'by'            => 'ip',
    'algorithm'     => 'sliding_window',
]);

// 4. Create middleware and register
$middleware = new RateLimitMiddleware($manager);
$router->registerMiddleware('rate-limit', $middleware, priority: 100);
$router->addGlobalMiddleware('rate-limit');

// 5. Initialize facade for programmatic use
RateLimiter::init($manager);
```

---

Quick Start — Using Rate Limits
-------------------------------

[](#quick-start--using-rate-limits)

### Attribute-Based (Recommended)

[](#attribute-based-recommended)

```
use MonkeysLegion\RateLimit\Attributes\RateLimit;
use MonkeysLegion\RateLimit\Algorithm;

#[RoutePrefix('/api/v2/users')]
final class UserController
{
    // 60 requests per minute, per IP (token bucket)
    #[Route('GET', '/', name: 'users.index')]
    #[RateLimit(maxAttempts: 60, windowSeconds: 60)]
    public function index(): Response { /* ... */ }

    // 5 login attempts per 5 minutes, per IP (sliding window)
    #[Route('POST', '/login', name: 'users.login')]
    #[RateLimit(maxAttempts: 5, windowSeconds: 300, algorithm: Algorithm::SlidingWindow)]
    public function login(): Response { /* ... */ }

    // Stacked: global IP limit + per-user per-route limit
    #[Route('POST', '/', name: 'users.create')]
    #[RateLimit(maxAttempts: 1000, windowSeconds: 3600, by: 'ip')]
    #[RateLimit(maxAttempts: 10, windowSeconds: 60, by: 'user+route')]
    public function create(): Response { /* ... */ }

    // Reference a named limiter from config
    #[Route('GET', '/search', name: 'users.search')]
    #[RateLimit(limiter: 'api')]
    public function search(): Response { /* ... */ }
}
```

### Programmatic Usage (Facade)

[](#programmatic-usage-facade)

```
use MonkeysLegion\RateLimit\RateLimiter;

// After bootstrap (RateLimiter::init() was called by the Provider)

$result = RateLimiter::attempt('login:192.168.1.1', maxAttempts: 5, windowSeconds: 300);

if ($result->exceeded) {
    echo "Try again in {$result->retryAfter} seconds";
}

// Check without consuming
if (RateLimiter::tooManyAttempts('api:10.0.0.1', 100, 60)) {
    // Already at limit
}

// Clear all state for a key (e.g., after password reset)
RateLimiter::clear('login:192.168.1.1');
```

### From Attribute (Standalone)

[](#from-attribute-standalone)

Use `fromAttribute()` to evaluate rate limits from a `#[RateLimit]` attribute without the middleware:

```
use MonkeysLegion\RateLimit\Attributes\RateLimit;
use MonkeysLegion\RateLimit\Algorithm;

$attr    = new RateLimit(maxAttempts: 100, windowSeconds: 60, by: 'ip+route');
$result  = $manager->fromAttribute($attr, $request);

if (!$result->allowed) {
    // Handle rate limit exceeded
}

// Works with named limiters too
$attr   = new RateLimit(limiter: 'api');
$result = $manager->fromAttribute($attr, $request);
```

---

Redis Unavailability — Resilient Storage
----------------------------------------

[](#redis-unavailability--resilient-storage)

> **Neither Laravel nor Symfony handle this.** Both throw 500 errors when Redis goes down. MonkeysLegion is the first PHP framework to ship a built-in resilient rate limiter.

### The Problem

[](#the-problem)

FrameworkWhen Redis is Down**Laravel**`ThrottleRequests` throws `ConnectionException` → **500 error****Symfony**Rate Limiter throws exception → **500 error****MonkeysLegion**Configurable: fail-open, fail-closed, or fallback — **your API keeps working**### Three Modes of Operation

[](#three-modes-of-operation)

ModeMLC ConfigBehavior When Redis Is Down**Fail-Open**`fail_open = true`All requests allowed — no rate limiting**Fail-Open + Fallback**`fail_open = true, fallback_driver = memory`Per-process rate limiting (no cross-worker consistency)**Fail-Closed**`fail_open = false`All requests denied with 429 — maximum security### Circuit Breaker Pattern

[](#circuit-breaker-pattern)

```
CLOSED (normal) → 3 consecutive failures → OPEN (skip Redis)
                                              ↓
                               30 seconds elapse
                                              ↓
                                    HALF-OPEN (probe Redis)
                                              ↓
                              Success → CLOSED  |  Failure → OPEN

```

### Development Without Redis

[](#development-without-redis)

```
# .env
RATE_LIMIT_STORAGE=memory
```

Or in the MLC file:

```
storage {
    driver = memory
}

```

> **Note**: `memory` driver is single-process. Rate limits are not shared across workers.

---

Router Integration
------------------

[](#router-integration)

The rate limiter hooks into the router via the `#[RateLimit]` attribute system:

```
Controller → ControllerScanner → Route Meta → Router Dispatch → Request Attribute → Middleware

```

### How It Works

[](#how-it-works)

1. **`ControllerScanner`** scans for `#[RateLimit]` attributes on controller classes and methods
2. Rate limit configs are stored in `meta['rate_limits']` on each `RouteDefinition`
3. **`Router::dispatch()`** attaches `_rate_limits` as a PSR-7 request attribute
4. **`RateLimitMiddleware`** reads `_rate_limits`, resolves keys, evaluates limits, returns 429 or adds headers

### Backwards Compatibility

[](#backwards-compatibility)

The existing `#[Throttle]` attribute continues to work — the scanner bridges it automatically:

```
// Old way — still works
#[Throttle(max: 60, per: 60, by: 'ip')]

// New way — more features
#[RateLimit(maxAttempts: 60, windowSeconds: 60, by: 'ip', algorithm: Algorithm::TokenBucket)]
```

---

Competitive Comparison
----------------------

[](#competitive-comparison)

FeatureMonkeysLegionLaravel 13Symfony 7Token Bucket✅❌ (fixed window)✅Sliding Window✅❌ (fixed window)✅Redis Lua Atomic✅✅ (Redis driver)❌ (uses locks)**Fail-Open Mode**✅❌ (500 error)❌ (500 error)**Circuit Breaker**✅❌❌**Fallback Storage**✅❌❌**MLC Config**✅YAML/PHPYAMLComposite Keys✅ (`ip+route`)❌ (manual)❌Stacked Limits✅ (repeatable attr)✅✅PHP 8.4 Hooks✅❌❌PSR-15 Standard✅❌ (custom)❌ (custom)Named Limiters✅✅✅IP Hashing (GDPR)✅❌❌---

Algorithms
----------

[](#algorithms)

### Token Bucket

[](#token-bucket)

Best for APIs that allow burst traffic while maintaining a steady average rate.

- **Capacity** = `maxAttempts` (burst size)
- **Refill rate** = `maxAttempts / windowSeconds` tokens per second
- Each request consumes `cost` tokens (default: 1)
- Tokens refill continuously based on elapsed time

### Sliding Window

[](#sliding-window)

Best for strict rate limiting with precise request counting.

- Tracks each request timestamp in a rolling window
- No burst allowance — exactly `maxAttempts` per `windowSeconds`
- Uses Redis sorted sets for O(log n) operations

Key Resolvers
-------------

[](#key-resolvers)

ResolverDescription`ip`Client IP (with trusted proxy support)`user`Authenticated user ID (falls back to IP)`route`Route pattern (e.g., `/api/users/{id}`)`ip+route`Per-IP per-route composite`user+route`Per-user per-route compositeResponse Headers
----------------

[](#response-headers)

Follows RFC 6585 + [draft-ietf-httpapi-ratelimit-headers](https://datatracker.ietf.org/doc/draft-ietf-httpapi-ratelimit-headers/).

On successful requests:

```
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 42
X-RateLimit-Reset: 1716350400
X-RateLimit-Policy: 60;w=60

```

On rate-limited requests (429):

```
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1716350400
X-RateLimit-Policy: 60;w=60
Retry-After: 30

```

HeaderDescription`X-RateLimit-Limit`Maximum requests allowed in the window`X-RateLimit-Remaining`Requests remaining in the current window`X-RateLimit-Reset`Unix timestamp when the window resets`X-RateLimit-Policy`Limit and window in `{limit};w={seconds}` format (draft spec)`Retry-After`Seconds until next request is allowed (only on 429)Telemetry Metrics
-----------------

[](#telemetry-metrics)

When telemetry is enabled, the middleware emits the following metrics via the callback:

MetricTypeLabels`ml_rate_limit_allowed_total`Counter`resolver`, `algorithm``ml_rate_limit_denied_total`Counter`resolver`, `algorithm``ml_rate_limit_remaining`Gauge`resolver`, `algorithm`, `remaining`, `limit````
// Custom telemetry callback example
$middleware = new RateLimitMiddleware($manager, metricsCallback: function (
    string $metricName,
    array $labels,
): void {
    // Emit to Prometheus, StatsD, or any monitoring backend
    Telemetry::counter($metricName, $labels);
});
```

Requirements
------------

[](#requirements)

- PHP ^8.4
- ext-redis (recommended for production)
- PSR-7 HTTP Message ^2.0
- PSR-15 HTTP Server Middleware ^1.0

License
-------

[](#license)

MIT License — see [LICENSE](LICENSE) for details.

###  Health Score

40

—

FairBetter than 86% of packages

Maintenance96

Actively maintained with recent releases

Popularity0

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity51

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

18d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/2913369?v=4)[Jorge Peraza](/maintainers/yorchperaza)[@yorchperaza](https://github.com/yorchperaza)

---

Top Contributors

[![yorchperaza](https://avatars.githubusercontent.com/u/2913369?v=4)](https://github.com/yorchperaza "yorchperaza (2 commits)")

---

Tags

middlewareredispsr-15rate limitthrottletoken bucketmonkeyslegionsliding-window

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Type Coverage Yes

### Embed Badge

![Health badge](/badges/monkeyscloud-monkeyslegion-rate-limit/health.svg)

```
[![Health](https://phpackages.com/badges/monkeyscloud-monkeyslegion-rate-limit/health.svg)](https://phpackages.com/packages/monkeyscloud-monkeyslegion-rate-limit)
```

###  Alternatives

[cakephp/cakephp

The CakePHP framework

8.8k19.1M1.7k](/packages/cakephp-cakephp)[cakephp/authentication

Authentication plugin for CakePHP

1153.9M95](/packages/cakephp-authentication)[eliashaeussler/typo3-warming

Warming - Warms up Frontend caches based on an XML sitemap. Cache warmup can be triggered via TYPO3 backend or using a console command. Supports multiple languages and custom crawler implementations.

22249.2k](/packages/eliashaeussler-typo3-warming)[eliashaeussler/typo3-solver

Solver - Extends TYPO3's exception handling with AI generated solutions. Problems can also be solved from command line. Several OpenAI parameters are configurable and prompts and solution providers can be customized as desired.

292.1k](/packages/eliashaeussler-typo3-solver)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
