web php

Rate Limiting

Rate limiting restricts how many requests a client can make in a period of time. It protects login forms, password reset forms, APIs, search endpoints, expensive reports, and any feature that can be abused or accidentally overloaded.

A rate limit usually needs a key, a limit, a time window, and a response when the limit is exceeded. The key might be a user ID, IP address, API token, route name, or a combination.

A simple fixed-window example

PHP example
<?php

declare(strict_types=1);

function isAllowed(int $currentCount, int $limit): bool
{
    return $currentCount < $limit;
}

foreach ([0, 4, 5] as $count) {
    echo $count . ': ' . (isAllowed($count, 5) ? 'allowed' : 'limited') . PHP_EOL;
}

// Prints:
// 0: allowed
// 4: allowed
// 5: limited

Real applications store counters in Redis, a database, a cache service, a framework limiter, or a gateway. An in-memory array is only useful for explaining the idea because each PHP process would have its own copy.

Choosing the key

Rate limiting by IP is common for anonymous traffic, but it can punish many users behind the same network. Rate limiting by user ID is better after authentication. API token limits are useful for partner or machine-to-machine APIs.

For login attempts, combine identifiers carefully: username/email plus IP is often more useful than IP alone.

Choose The Rule For The Risk

Different endpoints need different limits. A public product search may tolerate many requests. A password-reset endpoint should be much tighter because abuse can send unwanted email and help attackers probe accounts.

Common strategies include:

fixed window   count requests inside a time bucket
sliding window count requests across the recent period more smoothly
token bucket   allow a small burst while refilling capacity over time

Framework or gateway limiters usually provide these strategies. Start with a rule that is easy to explain and monitor, then adjust it from real traffic rather than guessing forever.

Shared Storage And Atomic Updates

A real limiter needs shared storage because requests can reach different PHP-FPM workers or different servers.

The counter update also needs to be atomic. Two requests arriving together must not both read the same old value and increment it independently. Redis is commonly used because increment-and-expire operations can be coordinated efficiently.

Rate limiting at a CDN or gateway is useful for broad traffic protection. Application-level limits are still useful when the key depends on authenticated user data or business rules.

The response

When a limit is exceeded, return 429 Too Many Requests. If possible, include a retry hint:

PHP example
<?php

declare(strict_types=1);

http_response_code(429);
header('Retry-After: 60');

echo 'Too many requests. Try again later.' . PHP_EOL;

// Output body:
// Too many requests. Try again later.

Do not reveal sensitive details, such as whether a username exists, in rate-limit responses.

What you should be able to do

After this lesson, you should be able to explain why rate limiting exists, choose a sensible key and strategy, know why shared storage and atomic updates matter, return a 429 response, and recognise endpoints that need tighter limits.

Practice

Task: Design A Login Rate Limit

Create a small PHP example or checklist for rate limiting login attempts.

Requirements

  • Choose a rate-limit key.
  • Define a request limit and time window.
  • Show allowed and blocked examples.
  • Include a 429 Too Many Requests response note.
  • Explain why shared storage is needed in a real application.

Check your work

The answer should protect the login form without relying on memory local to one PHP request.

Show solution
PHP example
<?php

declare(strict_types=1);

function loginLimitKey(string $email, string $ip): string
{
    return 'login:' . strtolower(trim($email)) . ':' . $ip;
}

function isAllowed(int $attemptsInWindow, int $limit): bool
{
    return $attemptsInWindow < $limit;
}

$key = loginLimitKey('Ada@Example.com', '203.0.113.10');

echo $key . PHP_EOL;
echo isAllowed(4, 5) ? 'allowed' : 'limited';
echo PHP_EOL;
echo isAllowed(5, 5) ? 'allowed' : 'limited';
echo PHP_EOL;

// Prints:
// login:ada@example.com:203.0.113.10
// allowed
// limited

The rule could be five attempts per email/IP pair per five minutes. When blocked, the response should be 429 Too Many Requests, optionally with Retry-After. In production, the counter belongs in shared storage such as Redis, not a PHP array, because multiple workers and requests need to see the same count.