Debug School

rakesh kumar
rakesh kumar

Posted on

Rate Limiting in Web Applications: Concepts, Use Cases, and Best Practices in laravel

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?”
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode
Goal: Block repeated wrong-password attempts.
Enter fullscreen mode Exit fullscreen mode
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']);
}
Enter fullscreen mode Exit fullscreen mode

2) Forgot password / reset throttling

Goal: Stop spamming password reset emails.
Enter fullscreen mode Exit fullscreen mode
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),
    ]);
}
Enter fullscreen mode Exit fullscreen mode

3) Registration throttling (bot signup protection)

Goal: Limit signups per IP.
Enter fullscreen mode Exit fullscreen mode
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]);
}
Enter fullscreen mode Exit fullscreen mode

4) Email verification resend throttle

Goal: Stop repeated “resend verification” clicks.
Enter fullscreen mode Exit fullscreen mode
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.']);
}
Enter fullscreen mode Exit fullscreen mode

5) API request throttling per user (authenticated)

Goal: Control API spam from one account.
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

Register middleware and apply to routes.

6) API request throttling per IP (public endpoints)

Goal: Protect public endpoints (no login required).
Enter fullscreen mode Exit fullscreen mode
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);
    }
}
Enter fullscreen mode Exit fullscreen mode

7) Search endpoint throttling (expensive query protection)

Goal: Stop someone from running heavy searches repeatedly.
Enter fullscreen mode Exit fullscreen mode
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]);
}
Enter fullscreen mode Exit fullscreen mode

8) Contact form spam prevention

Goal: Prevent repeated submissions.
Enter fullscreen mode Exit fullscreen mode
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.']);
}
Enter fullscreen mode Exit fullscreen mode

9) Comment/review/rating spam prevention

Goal: Stop 20 reviews in 1 minute.
Enter fullscreen mode Exit fullscreen mode

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.']);
}
Enter fullscreen mode Exit fullscreen mode

10) File upload throttling

Goal: Avoid CPU/storage abuse.
Enter fullscreen mode Exit fullscreen mode
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']);
}
Enter fullscreen mode Exit fullscreen mode

11) Webhook receiver throttling

Goal: Protect your webhook endpoint from floods.
Enter fullscreen mode Exit fullscreen mode
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]);
}
Enter fullscreen mode Exit fullscreen mode

12) Payment / checkout attempts throttling

Goal: Stop repeated checkout attempts.
Enter fullscreen mode Exit fullscreen mode
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']);
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

=================or=====================

                $gate = $this->utilityController->emailOtpSendGate($request, 5);

    if (!$gate['ok']) {
        return $gate['response'];
    }
Enter fullscreen mode Exit fullscreen mode
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
        ];
    }
Enter fullscreen mode Exit fullscreen mode
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
        ];
    }
Enter fullscreen mode Exit fullscreen mode

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.

chatgpt

Top comments (0)