Rate Limiting
By the end of this lesson you'll be able to protect a PHP API from abuse — choosing the right algorithm, counting hits with Redis, scoping limits per IP, user, or key, and returning a correct 429 with the headers clients expect.
Learn Python, JavaScript, Java and more with free interactive lessons, real projects and AI-powered help. Beginner-friendly.
Part of the free Php course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
What You'll Learn in This Lesson
1️⃣ Why Rate Limit At All?
Rate limiting caps how many requests a single client may make in a window of time. Without it, one script can do enormous damage: brute-force a login by trying thousands of passwords a second, run a denial-of-service (DDoS) attack that buries your server in traffic so real users can't get in, scrape your whole catalogue in minutes, or rack up a huge bill when every request hits a paid API or database. A limit turns all of those from "trivially easy" into "not worth trying."
2️⃣ Fixed Window — The Simplest Algorithm
A fixed window chops time into equal slots — say every 60 seconds — and keeps one counter per slot. Each request adds 1; once the counter passes the limit, the rest are denied until the clock ticks into the next slot, which starts a fresh count of zero. You don't need a cleanup job: the window number is baked into the key, so old windows simply stop being referenced. It's the easiest algorithm to get right, which is why it's the place to start.
The catch: a client can fire the full limit at the end of one window and the full limit again at the start of the next, doubling the real rate for a couple of seconds. That boundary burst is exactly what the next two algorithms fix.
3️⃣ Token Bucket — Allow Bursts, Cap the Average
The token bucket is the most widely used algorithm. Picture a bucket that holds up to capacity tokens and refills at a steady rate (say 2 tokens/second). Every request spends one token; if the bucket is empty, the request is denied. Because unused tokens build up, a client that's been quiet can burst — spend a pile of saved tokens at once — yet the steady refill rate still caps the long-term average. It's the friendliest behaviour for real users while staying strict on sustained abuse.
Watch the timeline: five requests drain the bucket, the sixth is rejected, then after one second two tokens have refilled, so request 7 succeeds and leaves one in reserve. A leaky bucket is the close cousin — instead of spending tokens, requests drip out of a queue at a constant rate, which smooths output even harder but can add latency while requests wait their turn.
4️⃣ Counting With Redis & Returning 429
A PHP variable lives for one request, so it can't track a client across many requests on many servers. The standard tool is Redis , a shared in-memory store every server can reach. Two commands do the work: INCR atomically adds 1 and returns the new total (so two requests landing at once can't both read a stale count), and EXPIRE auto-deletes the key when the window ends. When a client goes over, you return HTTP 429 Too Many Requests with a Retry-After header telling it how long to wait, plus X-RateLimit-* headers on every response so well-behaved clients throttle themselves.
The if ($count === 1) check is the key trick: only the request that creates the counter sets its expiry, so the window measures from the first hit and the key tidies itself up. (For very high traffic you'd run this as a small Lua script in Redis to make the INCR and EXPIRE a single atomic step.)
5️⃣ Per-IP, Per-User, Per-Key — and Where the Check Goes
The counter is only as good as the identity you count by. Limit per API key for trusted partners, per user ID for logged-in people (so a shared office IP doesn't punish everyone), and per IP only as the last resort for anonymous traffic. Just as important is where the check runs: put it in middleware at the very front of the pipeline so an over-limit request is rejected before it touches your router, controller, or database. Limit too late and you've already paid for the work you were trying to block.
6️⃣ Your Turn
Time to write the parts that matter. The window logic below is done — you just supply the allow/deny decision and the remaining count. Fill each ___ using the 👉 hint, then run it and check against the Output panel.
Now the rejection side. A client is over its limit — send back a correct 429. Fill in the status code and the two headers a polite response must carry.
📋 Quick Reference — Algorithms & Headers
No code is filled in this time — just a brief and an outline. Write it yourself, run it on onecompiler.com/php or your own machine, then check your result against the expected output in the comments. This is the write-run-check loop you'll use on every real limiter.
Practice quiz
What does rate limiting protect against?
- SQL injection only
- Cross-site scripting
- Abuse like brute-force logins, DDoS, scraping, and runaway API costs
- Slow database queries
Answer: Abuse like brute-force logins, DDoS, scraping, and runaway API costs. Capping requests per client turns brute-force, DDoS, scraping, and bill-exploding traffic from trivially easy into not worth trying.
What is the known weakness of the fixed-window algorithm?
- A client can burst the full limit at the end of one window and again at the start of the next
- It needs a cleanup job
- It cannot count requests
- It only works with Redis
Answer: A client can burst the full limit at the end of one window and again at the start of the next. The boundary burst lets a client double the real rate for a couple of seconds; sliding window or token bucket smooth this out.
Why does the token bucket allow short bursts?
- It ignores the limit during bursts
- It resets every second
- It doubles capacity under load
- Unused tokens accumulate, so a quiet client can spend saved-up tokens at once
Answer: Unused tokens accumulate, so a quiet client can spend saved-up tokens at once. Tokens build up while a client is quiet, so it can burst by spending them, yet the steady refill rate still caps the long-term average.
Why use Redis instead of a PHP variable for the counter?
- A PHP variable is slower
- Real apps run many processes/servers, so the counter must live in one shared store they all reach
- Redis encrypts the count
- PHP variables can't hold integers
Answer: Real apps run many processes/servers, so the counter must live in one shared store they all reach. A PHP variable lives for one request; Redis is shared across all servers, and INCR is atomic so concurrent requests can't read a stale count.
Why is Redis INCR used instead of GET-then-SET to count requests?
- INCR atomically adds 1 in one step, avoiding a race where two requests lose a hit
- INCR is shorter to type
- GET-then-SET doesn't work in Redis
- INCR also sends the 429 response
Answer: INCR atomically adds 1 in one step, avoiding a race where two requests lose a hit. GET-then-SET can let two requests read the same value and both write back the same number, losing a hit; INCR reads-adds-writes atomically.
Which HTTP status code means a client has sent too many requests?
- 403
- 404
- 429
- 503
Answer: 429. 429 Too Many Requests is the response when a client is over its limit.
Which header tells a client how long to wait before retrying after a 429?
- X-RateLimit-Limit
- Retry-After
- Content-Type
- Authorization
Answer: Retry-After. Always send Retry-After (seconds or a date) so a well-behaved client backs off instead of hammering you instantly.
When counting per-window with Redis, when do you set the key's EXPIRE?
- On every request
- Never — the key is permanent
- After the limit is exceeded
- Only on the first hit, when INCR returns 1
Answer: Only on the first hit, when INCR returns 1. Only the request that creates the counter (INCR returns 1) sets the expiry, so the window measures from the first hit and tidies itself up.
Which identity should you prefer when scoping a rate limit?
- Always the IP address
- API key if present, else user id, else IP as a last resort
- Always the IP plus user agent
- The session id only
Answer: API key if present, else user id, else IP as a last resort. Prefer the most specific identity — API key, then user id — so a shared office IP doesn't punish everyone; fall back to IP for anonymous traffic.
Where should the rate-limit check run for it to be effective?
- After the controller and database queries
- Inside the database
- In middleware at the very front, before routing or any expensive work
- Only in the browser
Answer: In middleware at the very front, before routing or any expensive work. Check in middleware at the front and return 429 early; limiting after the expensive work means you've already paid the cost you wanted to avoid.