Library

Token Bucket

Allow a short burst, then refill tokens at a steady rate. The burst cannot exceed the bucket size.

PHP Free to use
Code
final class TokenBucketLimiter
{
    /** @var array<string, array{tokens: float, updated_at: float}> */
    private array $buckets = [];

    public function __construct(
        private readonly int $capacity,
        private readonly float $refillPerSecond,
    ) {
        if ($capacity < 1 || $refillPerSecond <= 0) {
            throw new InvalidArgumentException('Capacity and refill rate must be positive.');
        }
    }

    public function allow(string $key, int $cost = 1): bool
    {
        return $this->take($key, $cost)['allowed'];
    }

    /**
     * @return array{allowed: bool, remaining: int, retry_after: int}
     */
    public function take(string $key, int $cost = 1): array
    {
        if ($cost < 1) {
            throw new InvalidArgumentException('Cost must be at least 1.');
        }

        $now = microtime(true);
        $bucket = $this->refill($this->buckets[$key] ?? $this->full($now), $now);

        if ($cost > $this->capacity) {
            $this->buckets[$key] = $bucket;

            return [
                'allowed' => false,
                'remaining' => (int) floor($bucket['tokens']),
                'retry_after' => 0,
            ];
        }

        $allowed = $bucket['tokens'] >= $cost;
        if ($allowed) {
            $bucket['tokens'] -= $cost;
        }

        $this->buckets[$key] = $bucket;
        $missing = $allowed ? 0.0 : ($cost - $bucket['tokens']);

        return [
            'allowed' => $allowed,
            'remaining' => (int) floor($bucket['tokens']),
            'retry_after' => $missing <= 0 ? 0 : (int) ceil($missing / $this->refillPerSecond),
        ];
    }

    public function remaining(string $key): int
    {
        $now = microtime(true);
        $bucket = $this->refill($this->buckets[$key] ?? $this->full($now), $now);
        $this->buckets[$key] = $bucket;

        return (int) floor($bucket['tokens']);
    }

    /** @param array{tokens: float, updated_at: float} $bucket */
    private function refill(array $bucket, float $now): array
    {
        $elapsed = max(0.0, $now - $bucket['updated_at']);
        $bucket['tokens'] = min(
            (float) $this->capacity,
            $bucket['tokens'] + ($elapsed * $this->refillPerSecond)
        );
        $bucket['updated_at'] = $now;

        return $bucket;
    }

    /** @return array{tokens: float, updated_at: float} */
    private function full(float $now): array
    {
        return [
            'tokens' => (float) $this->capacity,
            'updated_at' => $now,
        ];
    }
}
Quick try
$limiter = new TokenBucketLimiter(5, 1); // burst 5, then 1 token / second

$first = $limiter->take('user:42');
// ['allowed' => true, 'remaining' => 4, 'retry_after' => 0]

for ($i = 0; $i < 5; $i++) {
    $limiter->take('user:42');
}

$blocked = $limiter->take('user:42');
// ['allowed' => false, 'remaining' => 0, 'retry_after' => 1]

echo $limiter->remaining('user:42');

What it does

  • Capacity is the largest burst. The rate is how many tokens are added each second after that.
  • remaining refills from elapsed time. It does not return the last stored number as-is.
  • This copy lives in one process. To share it across workers, persist the bucket in Redis with a lock or a Lua script.

Where it fits

A practical place for this snippet in a real project.

Useful for a form endpoint or an API route inside the same process. It is not a front-door limit for every server.

Similar snippets

Connection Pool
PHP

Connection Pool

Reuse an idle connection inside the same process, instead of opening a new one for every query.

Password Hashing
PHP

Password Hashing

Store a password as a salted hash, with a pepper from app config. This is not reversible encryption.

Need a snippet that is not here?

Describe the problem or idea, and I will see if it belongs in the library.