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(2mo ago)00MITPHPPHP ^8.4

Since May 23Pushed 2mo 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 3w 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

38

—

LowBetter than 83% of packages

Maintenance87

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

63d 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.5M1.8k](/packages/cakephp-cakephp)[cakephp/authentication

Authentication plugin for CakePHP

1214.1M107](/packages/cakephp-authentication)[typo3/cms

TYPO3 CMS is a free open source Content Management Framework initially created by Kasper Skaarhoj and licensed under GNU/GPL.

1.2k1.9M122](/packages/typo3-cms)[sunrise/http-router

A powerful solution as the foundation of your project.

17451.8k11](/packages/sunrise-http-router)[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.

22260.2k](/packages/eliashaeussler-typo3-warming)[typo3/cms-adminpanel

TYPO3 CMS Admin Panel - The Admin Panel displays information about your site in the frontend and contains a range of metrics including debug and caching information.

115.7M68](/packages/typo3-cms-adminpanel)

PHPackages © 2026

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