Introduction
Why Rate Limiting Is Important
How Rate Limiting Works Conceptually
Key Components of a Rate Limiting System
Common Rate Limiting Algorithms
Real-World Use Cases of Rate Limiting
Benefits of Rate Limiting
Best Practices for Effective Rate Limiting
Quick list: common uses of RateLimiter
Practical larvel code example
Conclusion
Introduction
Modern web applications are expected to be fast, reliable, and secure while serving users from different locations, devices, and networks. As applications grow, they become increasingly exposed to automated traffic, bots, malicious scripts, and accidental misuse. Without proper safeguards, even a simple feature like login, search, or OTP verification can be exploited to overload systems or compromise security.
Rate limiting is a fundamental technique used to control how frequently requests can be made to an application or specific endpoint. It acts as a protective layer that ensures fair usage, prevents abuse, and maintains system stability. Rather than blocking users entirely, rate limiting introduces controlled boundaries that protect both the application and its users.
What Is Rate Limiting?
Rate limiting is the practice of restricting the number of requests a client can make to a server within a defined time window. These limits can be applied based on various identifiers such as IP address, user account, email address, phone number, or API key.
The primary goal of rate limiting is not to deny service but to regulate request flow. When a client exceeds the allowed request threshold, the system temporarily rejects or delays additional requests until the limit resets.
In simple terms, rate limiting answers one question:
“How often is too often?”
Why Rate Limiting Is Important
Without rate limiting, web applications are vulnerable to several risks:
Brute-force login attempts
OTP and verification code abuse
Automated bot traffic
Denial-of-service scenarios
Unexpected infrastructure costs
Rate limiting helps mitigate these risks by enforcing predictable and manageable request patterns. It ensures that system resources such as CPU, memory, database connections, and third-party services are used efficiently.
How Rate Limiting Works Conceptually
At a conceptual level, rate limiting works by tracking request counts over time. Each request is evaluated against a predefined rule consisting of:
An identifier – who is making the request
A limit – how many requests are allowed
A time window– over what duration the requests are counted
If the request count exceeds the limit within the given window, the system responds with a temporary rejection or delay.
Once the time window expires, the counter resets and requests are allowed again.
Key Components of a Rate Limiting System
Identifier
The identifier defines how requests are grouped. Common identifiers include:
IP address
Authenticated user ID
Email address
Phone number
API token
Combination of multiple attributes
Choosing the correct identifier is crucial for balancing security and user experience.
Request Limit
This defines the maximum number of requests permitted within a given period. Limits should be strict enough to prevent abuse but lenient enough to avoid blocking legitimate users.
Examples:
5 requests per minute
1 request every 30 seconds
100 requests per hour
Time Window
The time window determines how long requests are tracked before the count resets. Time windows can be short (seconds) or long (hours or days), depending on the endpoint’s sensitivity.
Enforcement Action
When a limit is exceeded, the system may:
Reject further requests temporarily
Return a specific error message
Ask the client to wait before retrying
Log the event for monitoring and analysis
Common Rate Limiting Algorithms
Fixed Window
Requests are counted in fixed time intervals. Once the interval resets, the count starts again.
Advantage: Simple to implement
Limitation: Allows traffic spikes at window boundaries
Sliding Window
Requests are tracked over a continuously moving time window rather than fixed blocks.
Advantage: More accurate request control
Limitation: Slightly more complex to manage
Token Bucket
Tokens are added to a bucket at a fixed rate. Each request consumes one token.
Advantage: Allows controlled bursts
Limitation: Requires token tracking
Leaky Bucket
Requests are processed at a constant rate, regardless of how quickly they arrive.
Advantage: Smooth traffic handling
Limitation: Less flexible for bursty traffic
Concurrency-Based Limiting
Limits the number of simultaneous active requests rather than total requests over time.
Advantage: Prevents server overload
Limitation: Not suitable for all use cases
Real-World Use Cases of Rate Limiting
Rate limiting is widely used across different parts of a web application:
Authentication systems to prevent brute-force attacks
OTP and verification endpoints to avoid SMS and email abuse
Password reset flows to stop repeated reset attempts
Registration forms to block automated signups
Search APIs to protect expensive database queries
Public APIs to ensure fair usage among clients
Payment and checkout processes to prevent repeated transaction attempts
Contact forms and feedback systems to reduce spam
Each use case requires carefully chosen limits based on risk and user behavior.
Benefits of Rate Limiting
Security Benefits
Reduces brute-force and credential-stuffing attacks
Limits bot activity and automated abuse
Protects sensitive endpoints
Performance Benefits
Prevents server overload
Maintains consistent response times
Improves application reliability
Cost Benefits
Reduces unnecessary SMS and email usage
Limits third-party API calls
Helps control infrastructure costs
User Experience Benefits
Ensures fair access for all users
Prevents service degradation during traffic spikes
Provides predictable system behavior
Best Practices for Effective Rate Limiting
Apply stricter limits to sensitive endpoints
Use meaningful identifiers rather than relying only on IP addresses
Combine rate limiting with request locks for critical actions
Provide clear, user-friendly error messages
Monitor and log rate-limit violations
Adjust limits based on real usage patterns
Avoid overly aggressive limits that harm legitimate users
Common Mistakes to Avoid
Setting limits too low and blocking real users
Using only IP-based limits in shared network environments
Ignoring distributed system considerations
Failing to reset limits after successful actions
Treating rate limiting as a standalone security solution
Quick list: common uses of RateLimiter
Login brute-force protection (email/username + IP)-Goal: Block repeated wrong-password attempts.
Password reset / forgot password throttling-Goal: Stop spamming password reset emails.
Registration throttling (stop bot signups)-Limit signups per IP.
Email verification resend throttle-Stop repeated “resend verification” clicks.
API request throttling per user (token/auth user)-Control API spam from one account.
API request throttling per IP (public endpoints)-Protect public endpoints (no login required).
Search endpoint throttling (expensive queries)-Stop someone from running heavy searches repeatedly
Contact form / feedback spam prevention-Prevent repeated submissions.-Prevent repeated submissions.
Comment/review/rating spam prevention-Stop 20 reviews in 1 minute
File upload throttling (heavy storage/CPU)-Avoid CPU/storage abuse.
Webhook receiver throttling (protect your app from floods)-Protect your webhook endpoint from floods.
Payment/checkout attempts throttling (stop abuse)-Stop repeated checkout attempts.
SMS/Email sending throttling (marketing/notifications)
Resend any “code” throttling (2FA, device verify, etc.)
Login brute-force protection (email + IP)
Goal: Block repeated wrong-password attempts.
Controller example
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Auth;
public function login(Request $request)
{
$request->validate([
'email' => ['required','email'],
'password' => ['required'],
]);
$email = Str::lower($request->email);
$key = "login:{$email}|ip:{$request->ip()}";
// 5 attempts allowed, lock for 60 seconds
if (RateLimiter::tooManyAttempts($key, 5)) {
$seconds = RateLimiter::availableIn($key);
return response()->json([
'success' => false,
'message' => "Too many attempts. Try again in {$seconds} seconds."
], 429);
}
if (!Auth::attempt(['email' => $email, 'password' => $request->password])) {
RateLimiter::hit($key, 60); // decay 60 sec
return response()->json(['success' => false, 'message' => 'Invalid credentials'], 401);
}
RateLimiter::clear($key); // ✅ success -> reset
return response()->json(['success' => true, 'message' => 'Login successful']);
}
2) Forgot password / reset throttling
Goal: Stop spamming password reset emails.
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Str;
public function forgotPassword(Request $request)
{
$request->validate(['email' => ['required','email']]);
$email = Str::lower($request->email);
$key = "pwreset:{$email}";
// 2 requests per 10 minutes
if (RateLimiter::tooManyAttempts($key, 2)) {
$seconds = RateLimiter::availableIn($key);
return response()->json([
'success' => false,
'message' => "Please wait {$seconds} seconds before requesting again."
], 429);
}
RateLimiter::hit($key, 600);
$status = Password::sendResetLink(['email' => $email]);
return response()->json([
'success' => $status === Password::RESET_LINK_SENT,
'message' => __($status),
]);
}
3) Registration throttling (bot signup protection)
Goal: Limit signups per IP.
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
public function register(Request $request)
{
$key = "register:ip:".$request->ip();
// 3 registrations per hour per IP
if (RateLimiter::tooManyAttempts($key, 3)) {
$seconds = RateLimiter::availableIn($key);
return response()->json([
'success' => false,
'message' => "Too many registrations from this IP. Try again in {$seconds} seconds."
], 429);
}
$request->validate([
'name' => ['required'],
'email' => ['required','email','unique:users,email'],
'password' => ['required','min:6'],
]);
RateLimiter::hit($key, 3600);
$user = User::create([
'name' => $request->name,
'email' => strtolower($request->email),
'password' => Hash::make($request->password),
]);
return response()->json(['success' => true, 'user' => $user]);
}
4) Email verification resend throttle
Goal: Stop repeated “resend verification” clicks.
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
public function resendVerification(Request $request)
{
$user = $request->user();
$key = "verify:resend:user:".$user->id;
// 1 resend per 60 seconds
if (RateLimiter::tooManyAttempts($key, 1)) {
$seconds = RateLimiter::availableIn($key);
return response()->json([
'success' => false,
'message' => "Please wait {$seconds} seconds."
], 429);
}
RateLimiter::hit($key, 60);
$user->sendEmailVerificationNotification();
return response()->json(['success' => true, 'message' => 'Verification email sent.']);
}
5) API request throttling per user (authenticated)
Goal: Control API spam from one account.
Middleware (simple custom)
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
class UserApiThrottle
{
public function handle(Request $request, Closure $next)
{
$userId = $request->user()?->id ?? 'guest';
$key = "api:user:{$userId}:".$request->path();
// 60 requests per minute
if (RateLimiter::tooManyAttempts($key, 60)) {
$seconds = RateLimiter::availableIn($key);
return response()->json([
'success' => false,
'message' => "Rate limit exceeded. Try again in {$seconds} seconds."
], 429);
}
RateLimiter::hit($key, 60);
return $next($request);
}
}
Register middleware and apply to routes.
6) API request throttling per IP (public endpoints)
Goal: Protect public endpoints (no login required).
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
class IpThrottle
{
public function handle(Request $request, Closure $next)
{
$key = "api:ip:".$request->ip().":".$request->path();
// 30 requests per minute
if (RateLimiter::tooManyAttempts($key, 30)) {
$seconds = RateLimiter::availableIn($key);
return response()->json([
'success' => false,
'message' => "Too many requests. Wait {$seconds}s."
], 429);
}
RateLimiter::hit($key, 60);
return $next($request);
}
}
7) Search endpoint throttling (expensive query protection)
Goal: Stop someone from running heavy searches repeatedly.
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
public function search(Request $request)
{
$q = trim((string)$request->query('q', ''));
$key = "search:ip:{$request->ip()}";
// 10 searches per minute per IP
if (RateLimiter::tooManyAttempts($key, 10)) {
$seconds = RateLimiter::availableIn($key);
return response()->json(['success' => false, 'message' => "Wait {$seconds}s"], 429);
}
RateLimiter::hit($key, 60);
// your heavy query here...
// $results = Model::where(...)->paginate(20);
return response()->json(['success' => true, 'query' => $q]);
}
8) Contact form spam prevention
Goal: Prevent repeated submissions.
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
public function contact(Request $request)
{
$key = "contact:ip:".$request->ip();
// 3 submissions per 10 minutes
if (RateLimiter::tooManyAttempts($key, 3)) {
$seconds = RateLimiter::availableIn($key);
return response()->json(['success' => false, 'message' => "Wait {$seconds}s"], 429);
}
RateLimiter::hit($key, 600);
// Store / email message...
return response()->json(['success' => true, 'message' => 'Thanks, we received your message.']);
}
9) Comment/review/rating spam prevention
Goal: Stop 20 reviews in 1 minute.
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
public function postReview(Request $request)
{
$userId = $request->user()->id;
$key = "review:user:{$userId}";
// 2 reviews per minute
if (RateLimiter::tooManyAttempts($key, 2)) {
$seconds = RateLimiter::availableIn($key);
return response()->json(['success' => false, 'message' => "Wait {$seconds}s"], 429);
}
RateLimiter::hit($key, 60);
// Save review...
return response()->json(['success' => true, 'message' => 'Review posted.']);
}
10) File upload throttling
Goal: Avoid CPU/storage abuse.
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
public function upload(Request $request)
{
$key = "upload:user:".$request->user()->id;
// 5 uploads per 10 minutes
if (RateLimiter::tooManyAttempts($key, 5)) {
$seconds = RateLimiter::availableIn($key);
return response()->json(['success' => false, 'message' => "Wait {$seconds}s"], 429);
}
RateLimiter::hit($key, 600);
$request->validate(['file' => ['required','file','max:5120']]);
// store file...
return response()->json(['success' => true, 'message' => 'Uploaded']);
}
11) Webhook receiver throttling
Goal: Protect your webhook endpoint from floods.
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
public function webhook(Request $request)
{
$source = $request->header('X-Provider', 'unknown');
$key = "webhook:source:{$source}";
// 120 webhooks per minute
if (RateLimiter::tooManyAttempts($key, 120)) {
return response()->json(['success' => false, 'message' => 'Too many webhook calls'], 429);
}
RateLimiter::hit($key, 60);
// Process webhook...
return response()->json(['success' => true]);
}
12) Payment / checkout attempts throttling
Goal: Stop repeated checkout attempts.
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
public function checkout(Request $request)
{
$userId = $request->user()->id;
$key = "checkout:user:{$userId}";
// 5 checkout attempts per 15 minutes
if (RateLimiter::tooManyAttempts($key, 5)) {
$seconds = RateLimiter::availableIn($key);
return response()->json(['success' => false, 'message' => "Try later in {$seconds}s"], 429);
}
RateLimiter::hit($key, 900);
// proceed payment...
return response()->json(['success' => true, 'message' => 'Checkout started']);
}
Pro tips for your OTP use-case (small improvements)
✅ Use IP + identifier combined key:
otp:send::ip:
otp📧send::ip:
✅ On success, optionally RateLimiter::clear($key) only if you want to allow resend immediately after success (usually NOT needed for OTP).
✅ Ensure your cache driver supports locks well (Redis is best). If you’re on file cache, locks can be weak.
Practical larvel code example
$gate = $this->utilityController->otpSendGate($request, 5);
if (!$gate['ok']) {
return $gate['response']; // stop here
}
// Generate OTP and store in session
$otp = rand(100000, 999999);
Session::put('phone_otp', $otp);
log::info("Generated OTP: " . $otp);
=================or=====================
$gate = $this->utilityController->emailOtpSendGate($request, 5);
if (!$gate['ok']) {
return $gate['response'];
}
public function otpSendGate(Request $request, int $cooldownSeconds = 5)
{
$phoneRaw = (string) $request->input('phone', '');
$phoneKey = preg_replace('/\D+/', '', $phoneRaw);
if ($phoneKey === '') {
return [
'ok' => false,
'response' => response()->json([
'success' => false,
'message' => 'Phone number is required'
], 422),
];
}
// 1) Rate limit: 1 request per 5 seconds
$rateKey = 'otp:send:' . $phoneKey;
if (RateLimiter::tooManyAttempts($rateKey, 1)) {
$seconds = RateLimiter::availableIn($rateKey);
return [
'ok' => false,
'response' => response()->json([
'success' => false,
'message' => "Please wait {$seconds} second(s) before requesting OTP again."
], 429),
];
}
RateLimiter::hit($rateKey, $cooldownSeconds);
// 2) Lock: prevents ms double-click / parallel calls
$lock = Cache::lock('otp:lock:' . $phoneKey, $cooldownSeconds);
if (!$lock->get()) {
return [
'ok' => false,
'response' => response()->json([
'success' => false,
'message' => 'OTP is already being sent. Please wait.'
], 429),
];
}
return [
'ok' => true,
'lock' => $lock,
'phoneKey' => $phoneKey
];
}
public function emailOtpSendGate(Request $request, int $cooldownSeconds = 5)
{
$emailRaw = strtolower(trim((string) $request->input('email', '')));
if (!filter_var($emailRaw, FILTER_VALIDATE_EMAIL)) {
return [
'ok' => false,
'response' => response()->json([
'success' => false,
'message' => 'Valid email is required'
], 422),
];
}
$emailKey = sha1($emailRaw); // safe cache key
/* 1️⃣ Rate limit: 1 OTP per 5 sec */
$rateKey = 'otp:email:send:' . $emailKey;
if (RateLimiter::tooManyAttempts($rateKey, 1)) {
$seconds = RateLimiter::availableIn($rateKey);
return [
'ok' => false,
'response' => response()->json([
'success' => false,
'message' => "Please wait {$seconds} second(s) before requesting OTP again."
], 429),
];
}
RateLimiter::hit($rateKey, $cooldownSeconds);
/* 2️⃣ Lock: prevents ms double-click */
$lock = Cache::lock('otp:email:lock:' . $emailKey, $cooldownSeconds);
if (!$lock->get()) {
return [
'ok' => false,
'response' => response()->json([
'success' => false,
'message' => 'OTP is already being sent. Please wait.'
], 429),
];
}
return [
'ok' => true,
'lock' => $lock,
'email' => $emailRaw
];
}
Conclusion
Rate limiting is a critical component of modern web application architecture. It protects systems from abuse, enhances performance, and ensures fair access to resources. When designed thoughtfully, rate limiting improves security without compromising user experience.
Rather than being an optional feature, rate limiting should be considered a foundational practice for any scalable, secure, and cost-effective web application.
Top comments (0)