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
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
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 Requestsresponse 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
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.