Skip to content

toggio/SimpleRateLimiter

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

8 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

SimpleRateLimiter

A small PHP class for Redis-based token bucket rate limiting and concurrency limiting.

Overview

SimpleRateLimiter protects PHP endpoints with two Redis-backed controls:

  • Rate limit: limits accepted requests with token buckets.
  • Concurrency limit: limits how many requests can run at the same time.

Both controls support one global rule for the protected action plus multiple identity rules, such as IP addresses, usernames, tenants, sessions, and API keys. By default, the class applies only global limits.

Requirements

  • PHP 5.6 or higher
  • Redis server
  • PHP Redis extension

Installation

Install Redis and the PHP Redis extension for your platform.

On Debian/Ubuntu:

sudo apt-get update
sudo apt-get install redis-server php-redis

Restart your web server or PHP-FPM after installing the extension.

Add SimpleRateLimiter.php to your project and include it where needed:

require_once __DIR__ . '/SimpleRateLimiter.php';

Default Behavior

With no options, new SimpleRateLimiter() uses these limits:

  • Rate global: bucket capacity 1000, refilled over 60 seconds.
  • Concurrency global: 100 simultaneous requests.
  • Redis: 127.0.0.1:6379, timeout 1.0 second.
  • Name: $_SERVER['SCRIPT_FILENAME'], or the class file when unavailable.

The default concurrency TTL is 60 seconds. It is a safety net for interrupted requests.

The constructor accepts either no argument or one options array. Positional arguments are not supported.

Production Usage

Use a stable name for each protected action and a project-specific key_prefix when Redis is shared with other applications. If your application is behind a reverse proxy, pass an IP address that your application has already resolved from a trusted proxy configuration. Do not trust X-Forwarded-For directly from the public internet.

Use release() whenever concurrency limiting is enabled. If only rate limiting is enabled, release() is not needed.

For a single protected page or a controlled block, use try / finally:

if (!$limiter->acquire()) {
    http_response_code(429);
    exit('Too many requests.');
}

try {
    require __DIR__ . '/protected-page.php';
} finally {
    $limiter->release();
}

For a whole site loaded through auto_prepend_file, .user.ini, .htaccess, or a shared bootstrap include, register the release once and let PHP call it at shutdown:

if (!$limiter->acquire()) {
    http_response_code(429);
    exit('Too many requests.');
}

register_shutdown_function(function () use ($limiter) {
    try {
        $limiter->release();
    } catch (Exception $e) {
        error_log('SimpleRateLimiter release failed: ' . $e->getMessage());
    }
});

Keep name for the protected action, such as login or api:v1:orders. Use global options for action-wide limits and rule-level scope + key values for dynamic identities such as IPs, usernames, or tenant IDs.

Examples

Production-oriented examples are in the examples/ directory:

  • examples/protected-endpoint.php: rate limit and concurrency limit, both per-IP and global.
  • examples/rate-limit-only.php: token bucket rate limit without concurrency slots.
  • examples/concurrency-limit-only.php: concurrency slots without rate counters.
  • examples/custom-scopes.php: limits by username, IP, tenant, and global rules.
  • examples/auto-prepend-bootstrap.php: site-wide bootstrap for auto_prepend_file or shared includes.
  • examples/debug-status-snapshot.php: CLI-only diagnostic example showing one debugStatus() snapshot.
  • examples/wordpress-front-controller.php: front-controller pattern for a WordPress-style index.php.

For WordPress-style usage, rename the existing index.php to index-original.php, put the limiter wrapper in its place, and adjust the two require paths to match your installation.

Redis Authentication

$limiter = new SimpleRateLimiter(array(
    'name' => 'admin:login',
    'global_rate_limit' => array('limit' => 200, 'window' => 60),
    'rate_limits' => array(
        'ip' => array('scope' => 'ip', 'key' => $_SERVER['REMOTE_ADDR'], 'limit' => 10, 'window' => 60),
    ),
    'global_concurrency_limit' => array('max_concurrent' => 20, 'ttl' => 60),
    'concurrency_limits' => array(
        'ip' => array('scope' => 'ip', 'key' => $_SERVER['REMOTE_ADDR'], 'max_concurrent' => 2, 'ttl' => 60),
    ),
    'redis_host' => '127.0.0.1',
    'redis_port' => 6379,
    'redis_auth' => getenv('REDIS_PASSWORD'),
    'redis_database' => 0,
));

Options

Identity

  • name (string): Stable identifier for the protected action. Defaults to the current script filename.
  • key_prefix (string): Redis key prefix. Default: simple_rate_limiter.

Keep name and rule names stable after deployment. Identity rule names, such as ip, user, or tenant, are part of the Redis key namespace. Renaming a rule starts a new bucket or slot counter even if scope and key stay the same.

Rate Limit

  • enable_rate_limit (bool): Enable request rate limiting. Default: true.
  • global_rate_limit (array|false): Action-wide rate rule. Default: array('limit' => 1000, 'window' => 60). Pass false to disable it. If window is omitted, 60 seconds is used.
  • rate_limits (array): Optional list of identity rate rules.
  • rate_limit (int): Fallback identity rate limit when a rate_limits rule omits limit. Default: 120.
  • rate_window (int): Fallback identity refill period when a rate_limits rule omits window. Default: 60.

Each rate_limits rule accepts:

  • scope: Identity label such as ip, user, tenant, session, or api_key.
  • key: Identity value, such as $_SERVER['REMOTE_ADDR'], $username, or $tenantId.
  • limit: Bucket capacity and maximum immediate burst.
  • window: Refill period in seconds. The bucket refills from empty to full over this period.

The rate limiter uses token buckets stored in Redis. limit is the bucket capacity and window is the refill period. For example, limit => 10 and window => 60 allows a burst of up to 10 requests, then refills at 10 tokens per minute. The global rule and all identity rules are checked atomically. A token is consumed only when every rule allows the request.

Concrete rate rule example:

'global_rate_limit' => array('limit' => 5000, 'window' => 60),
'rate_limits' => array(
    'ip' => array('scope' => 'ip', 'key' => $_SERVER['REMOTE_ADDR'], 'limit' => 30, 'window' => 60),
    'user' => array('scope' => 'user', 'key' => $username, 'limit' => 5, 'window' => 300),
    'tenant' => array('scope' => 'tenant', 'key' => $tenantId, 'limit' => 500, 'window' => 60),
)

Concurrency Limit

  • enable_concurrency_limit (bool): Enable concurrent request limiting. Default: true.
  • global_concurrency_limit (array|false): Action-wide concurrency rule. Default: array('max_concurrent' => 100, 'ttl' => 60). Pass false to disable it. If ttl is omitted, 60 seconds is used.
  • concurrency_limits (array): Optional list of identity concurrency rules.
  • max_concurrent (int): Fallback identity concurrency limit when a concurrency_limits rule omits max_concurrent. Default: 10.
  • concurrency_ttl (int): Fallback identity safety TTL when a concurrency_limits rule omits ttl. Default: 60.

Each concurrency_limits rule accepts:

  • scope: Identity label such as ip, user, tenant, session, or api_key.
  • key: Identity value, such as $_SERVER['REMOTE_ADDR'], $username, or $tenantId.
  • max_concurrent: Maximum simultaneous requests.
  • ttl: Safety TTL in seconds.

Always release concurrency slots after a successful acquire(), either in a finally block or through register_shutdown_function() when using a site-wide bootstrap. The TTL is only a safety net for fatal errors and interrupted requests. Set it higher than the maximum expected runtime of the protected action; if a request runs longer than its TTL, Redis can expire the slot before release() runs.

Concrete concurrency rule example:

'global_concurrency_limit' => array('max_concurrent' => 200, 'ttl' => 60),
'concurrency_limits' => array(
    'ip' => array('scope' => 'ip', 'key' => $_SERVER['REMOTE_ADDR'], 'max_concurrent' => 10, 'ttl' => 60),
    'user' => array('scope' => 'user', 'key' => $username, 'max_concurrent' => 1, 'ttl' => 60),
    'tenant' => array('scope' => 'tenant', 'key' => $tenantId, 'max_concurrent' => 50, 'ttl' => 60),
)

Defaults And Disabling

With no identity rules, the class creates only the global rate and concurrency rules. To disable those global rules:

new SimpleRateLimiter(array(
    'global_rate_limit' => false,
    'global_concurrency_limit' => false,
));

Use explicit global_rate_limit, global_concurrency_limit, rate_limits, and concurrency_limits for production configuration.

Unknown top-level options and unknown rule options are rejected. This helps catch configuration typos before the limiter silently falls back to defaults.

Delay

  • use_delay (bool): Add a delay as concurrency usage increases. Default: false.
  • min_delay (float): Minimum delay in seconds. Default: 0.
  • max_delay (float): Maximum delay in seconds. Default: 0.5.

Delay is applied only after all enabled limits have accepted the request. It throttles accepted requests; it does not retry rejected requests or turn a future 429 into a delayed success.

For public HTTP endpoints, returning 429 Too Many Requests is usually preferable to delaying requests.

Redis

  • redis_host (string): Redis host. Default: 127.0.0.1.
  • redis_port (int): Redis port. Default: 6379.
  • redis_timeout (float): Connection timeout in seconds. Default: 1.0.
  • redis_auth (string): Optional Redis password.
  • redis_database (int): Optional Redis database index.
  • redis (object): Optional existing Redis-compatible client.

Methods

acquire()

Returns true when the request is allowed. Returns false when any enabled rule is exceeded.

release()

Releases concurrency slots acquired by the current instance. It is safe to call when concurrency limiting is disabled or when no concurrency slot was acquired.

debugStatus()

Returns a structured snapshot of the limiter state, including identity, rate tokens, concurrency rules, rule identities, TTL values, Redis keys, and delay settings. This is intended for logs and internal diagnostics; do not expose it directly in public HTTP responses. The payload includes Redis key names, which should remain server-side.

Rate token values in debugStatus() are diagnostic snapshots. The limiter uses Redis time inside atomic Lua scripts; the debug output estimates the displayed token count at read time and can differ slightly from the next acquire() result.

Internal Testing

The scripts/ directory contains internal QA tooling for this repository. It is not production example code and is excluded from exported archives through .gitattributes.

Run the CLI checks with:

php scripts/test-limits.php

The script requires PHP, Redis, and the PHP Redis extension. HTTP integration helpers also live in scripts/ for local and remote verification during development.

The HTTP test endpoint is locked by default. Set SIMPLE_RATE_LIMITER_TEST_TOKEN on the server and pass the same token to the client:

php scripts/test-http-client.php https://example.com/http-test-endpoint.php --token=change-me

Help us

If you find this project useful and would like to support its development, consider making a donation. Any contribution is greatly appreciated!

Bitcoin (BTC) Addresses:

  • 1LToggiof3rNUTCemJZSsxd1qubTYoSde6
  • 3LToggio7Xx8qMsjCFfiarV4U2ZR9iU9ob

License

SimpleRateLimiter library is licensed under the Apache License, Version 2.0. You are free to use, modify, and distribute the library in compliance with the license.

Copyright (C) 2024-2026 Luca Soltoggio - https://www.lucasoltoggio.it/

About

A PHP class for Redis-based token bucket rate limiting with delay support

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages