-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions
-
Section 4: Object-Oriented PHP
-
Section 5: Working with Data
-
Section 6: Modern PHP (PHP 8)
-
Section 7: Working with Files and Networking
-
Section 8: Common PHP Frameworks Overview
-
Section 9: Tooling and Ecosystem
-
Section 10: Practical Projects
-
Section 11: Interview Practice
-
Section 12: More Standard Library
-
Section 13: Data Structures and Algorithms in PHP
-
Section 14: More Practice Exercises
-
Section 15: More Security and Best Practices
-
Section 16: More Testing and Tooling
-
Section 17: WordPress-Style CMS Concepts
-
Section 18: Advanced OOP Practice
-
Section 19: More Web Fundamentals
-
Section 20: Database Practice
-
Section 21: PHP Manual: Array Functions
-
Section 22: PHP Manual: Date and Calendar Functions
-
Section 23: PHP Manual: Filesystem and Directory Functions
-
Section 24: PHP Manual: Filter and Var Handling
-
Section 25: PHP Manual: Math Functions
-
Section 26: PHP Manual: JSON and XML
-
Section 27: PHP Manual: Network and Stream Functions
-
Section 28: PHP Manual: Error and Exception Handling
-
Section 29: PHP Manual: Output Control and Misc
-
Section 30: PHP Manual: FTP, Zip, and Mail
-
Section 31: Modern PHP Frameworks Deep Dive
-
Section 32: PHP Design Patterns
-
Section 33: More Practice Exercises
-
Section 34: PHP Performance and Deployment
-
Section 35: More Interview Practice
-
Section 36: More PHP Standard Library
-
Section 37: PHP Concurrency and Async
-
Section 38: More Web Development Practice
-
Section 39: PHP Testing Deep Dive
-
Section 40: Composer and Package Development
-
Section 41: PHP Security Deep Dive
-
Section 42: More Practical Projects
-
Section 43: Legacy PHP Maintenance
-
Section 44: More Algorithm Practice
-
Section 45: Final Practice and Review
-
Section 46: PHP for E-Commerce Patterns
-
Section 47: PHP API Design Deep Dive
-
Section 48: PHP Caching Strategies
-
Section 49: PHP Queue and Background Jobs
-
Section 50: PHP Multi-Tenancy Patterns
-
Section 51: PHP Real-Time Features
-
Section 52: PHP CMS and Content Modeling
-
Section 53: PHP Internationalization
-
Section 54: More Framework-Specific Practice
-
Section 55: PHP Legacy Code Refactoring
-
Section 56: More Practice Projects Round 2
-
Section 57: PHP Command-Line Applications
-
Section 58: PHP and Microservices
-
Section 59: More Interview and Review Round 2
141: Practice Exercise: Building a Rate Limiter Middleware
You've likely reached a point in your project where you're worried about someone (or some bot) hammering your API endpoints and crashing your server. A rate limiter middleware is the standard way to handle this. But when I first started writing these, I made a classic mistake that effectively turned my rate limiter into a permanent ban hammer.
Take a look at this snippet. It looks reasonable at first glance: it checks a cache for the user's IP, increments the count, and blocks them if they've hit the limit.
public function handle($request, Closure $next)
{
$ip = $request->ip();
$key = "rate_limit:" . $ip;
$requests = Cache::get($key, 0);
if ($requests >= 60) {
return response('Too Many Requests', 429);
}
Cache::put($key, $requests + 1);
return $next($request);
}
The Permanent Ban Bug
If you run this code, it will work perfectly for the first 60 requests. But once a user hits that 61st request, they are blocked. And here is the kicker: they are blocked forever. Or at least, until you manually clear your cache.
The problem is that I'm using Cache::put() without an expiration time. In a rate limiter, the "window" is everything. If you're allowing 60 requests per minute, that counter needs to vanish after 60 seconds. Because I didn't set a Time-To-Live (TTL), the key stays in Redis or Memcached indefinitely. The user hits 60, the count stops incrementing because they're blocked, but the value never drops back to 0.
Implementing a Sliding Window with TTL
To fix this, we need to ensure the key expires. However, simply adding a timeout to put() every time is also problematic because it resets the timer on every single request, potentially giving the user an infinite window if they keep requesting just under the limit.
The professional way to handle this is to check if the key exists first. If it doesn't, we set the initial value and the expiration. If it does, we just increment it. Here is how I'd actually write this to be robust:
public function handle($request, Closure $next)
{
$ip = $request->ip();
$key = "rate_limit:" . $ip;
$limit = 60;
$decayMinutes = 1;
// Use atomic increments if your cache driver supports it
$current = Cache::get($key);
if ($current && $current >= $limit) {
return response('Too Many Requests', 429);
}
if (!$current) {
// First request in this window: set the value and the expiration
Cache::put($key, 1, now()->addMinutes($decayMinutes));
} else {
// Subsequent requests: just increment
Cache::increment($key);
}
return $next($request);
}
I switched to Cache::increment() because it's atomic. In a high-traffic environment, if two requests hit at the exact same millisecond, a standard get and put cycle can suffer from a race condition where both requests read "5", both add 1, and both write "6"—effectively losing one count. increment() happens inside the cache store, ensuring no request is missed.
One thing to keep in mind: using the IP address is a great start, but in the real world, many users might share a single public IP (like in a corporate office). If you're building something for a paid service, you'll want to swap $request->ip() for a user_id from the authenticated session.
📋 Practical Task
Exercise: Building a Tiered API Rate Limiter
Your goal is to expand the basic rate limiter into a tiered system. Instead of a flat limit for everyone, you need to implement a middleware that checks the user's subscription level and applies different limits accordingly.
Requirements:
- Create a middleware that identifies if a user is 'guest', 'basic', or 'premium'. (For the sake of this exercise, you can simulate this by checking a
'plan'attribute on the User object or a custom header in the request). - Apply the following limits per minute:
- Guest: 10 requests
- Basic: 60 requests
- Premium: 1000 requests
- The middleware must use a cache store with a 1-minute expiration.
- If the limit is exceeded, return a 429 response, but include a custom header
X-RateLimit-Limittelling the user what their specific limit was. - Ensure you use atomic increments to prevent race conditions.
There are no comments for now.