php 128 lines · 3 tabs

PHP Login Rate Limiting with a Sliding-Window Throttle Middleware

Shared by codesnips Jul 2026
3 tabs
<?php

declare(strict_types=1);

namespace App\Security;

final class LimitResult
{
    public function __construct(
        public readonly bool $exceeded,
        public readonly int $hits,
        public readonly int $limit,
        public readonly int $retryAfter
    ) {
    }
}

final class SlidingWindowLimiter
{
    public function __construct(
        private \Redis $redis,
        private int $limit = 5,
        private int $windowSeconds = 60
    ) {
    }

    public function hit(string $key): LimitResult
    {
        $now = microtime(true);
        $cutoff = $now - $this->windowSeconds;
        $redisKey = 'throttle:' . $key;

        $this->redis->multi(\Redis::PIPELINE);
        $this->redis->zRemRangeByScore($redisKey, '0', (string) $cutoff);
        $this->redis->zAdd($redisKey, $now, sprintf('%.6f-%s', $now, bin2hex(random_bytes(4))));
        $this->redis->zCard($redisKey);
        $this->redis->expire($redisKey, $this->windowSeconds);
        $result = $this->redis->exec();

        $hits = (int) ($result[2] ?? 0);

        return new LimitResult(
            exceeded: $hits > $this->limit,
            hits: $hits,
            limit: $this->limit,
            retryAfter: $this->windowSeconds
        );
    }
}
3 files · php Explain with highlit

This snippet implements per-identity rate limiting for a login route using a PSR-15 middleware backed by Redis. The goal is to slow down credential-stuffing and brute-force attacks without locking out legitimate users, and to do it in a place that every login request must pass through regardless of controller logic.

The SlidingWindowLimiter class in the first tab implements the counting strategy. A simple fixed-window counter (one key per minute) suffers from a burst problem: an attacker can send the full quota at the end of one window and again at the start of the next, effectively doubling the allowed rate at the boundary. To avoid that, hit uses a Redis sorted set where each request is stored with its timestamp as the score. On each call it removes entries older than the window with zRemRangeByScore, adds the current request via zAdd, and reads the count with zCard. The whole sequence runs inside a MULTI/EXEC pipeline so concurrent requests cannot interleave and undercount, and an expire keeps the key from lingering. The returned LimitResult exposes whether the caller exceeded the limit plus a retryAfter so callers can advise clients when to try again.

The ThrottleLoginMiddleware in the second tab wires the limiter into the request pipeline. It builds a composite key from the client IP and the submitted username, so an attacker rotating usernames from one IP and one rotating IP against one account are both constrained. When the limit is exceeded it short-circuits with a 429 Too Many Requests, sets a Retry-After header, and never touches the authentication handler. Note that it only throttles POST requests to the login path — GET requests that render the form pass straight through.

The third tab, routes.php, shows the middleware bound only to the login endpoint rather than applied globally, which keeps the hot path for normal traffic cheap.

A key trade-off is that keying on IP is imperfect behind NAT or shared proxies, so clientIp prefers a trusted forwarded header when configured. The sorted-set approach costs more memory than a plain counter because it stores one member per request, which is why the window and limit should stay small. For production, pairing this network-layer throttle with progressive delays or CAPTCHA after repeated failures gives defense in depth.


Related snips

Share this code

Here's the card — post it anywhere.

PHP Login Rate Limiting with a Sliding-Window Throttle Middleware — share card
Link copied