IP-based limits are a baseline, not a solution — the only reliable way to rate limit traffic is to figure out who is actually sending each request (using an API Key, User ID, or a persistent Cookie/Session) and control them based on that, not on the network address they happen to be using.
Every engineer's first rate limiter looks the same: look at the visitor's IP address (think of it like the "street address" a request is coming from), count how many times that address showed up in the last minute, and block it if it shows up too much. It's quick to build, and it feels like it's working — until it doesn't.
The problem is that an IP address doesn't actually tell you "this is one person." It only tells you "this request came through this particular door." Lots of different real people can walk in through the same door, and one troublemaker can easily use a different door every single time. Once you see that, the simple approach stops looking clever and starts looking like a guessing game.
🏢 A whole office, school, or apartment building can share one single IP address for thousands of different people.
🌍 A bad actor can rent access to millions of different IP addresses for just a few dollars, and switch between them constantly.
📱 A phone on mobile data can get handed a brand-new IP address every few minutes just from walking around.
🔑 The whole trick: stop watching the door someone walks through, and start watching the person walking through it.
📑 In This Post
- 1. The Simple (But Broken) Way — Limiting by IP
- 2. The Bouncer Story
- 3. The Fix — Limiting by Who Someone Is
- 4. Old Way vs New Way, Side by Side
- 5. Where This Fits in Your App
- 6. Step-by-Step: Building It
- 7. The Code
- 8. How Do You Know It's Actually Working?
- 9. What's New in 2026
- FAQ
- Final Summary
1. The Simple (But Broken) Way — Limiting by IP
Most rate limiters start out really simple: keep a little counter next to each visitor's IP address. Every time a request comes in, add one to that counter. If the counter gets too high in, say, one minute, start saying "no" to that IP for a while. It works great in a small test, and it breaks the moment lots of real people show up.
1.1 Why This Breaks — In Plain Words
Three everyday things quietly ruin IP-based limiting:
Shared addresses. Most homes and offices actually share one public IP address between everyone inside, because of how the internet hands out addresses (this trick is called NAT — it's just a technique that lets many devices share one address). So one noisy person can accidentally get an entire building blocked.
Address-hopping attackers. People trying to cheat or attack a system can rent huge lists of IP addresses and jump between them nonstop, so your counter never sees the same "address" twice — it always looks like a brand-new visitor.
Phones changing addresses on their own. Cell phone networks hand out new IP addresses all the time as your phone moves around. So the very same real person might look like three or four "different visitors" during one shopping trip on their phone.
2. The Bouncer Story
💡 The Nightclub Bouncer
Imagine a bouncer at a club who only remembers people by which street they walked in from — not by their face, not by any ID. If fifty different people all walk in from the same street, he assumes they must all be the same troublemaker, and locks the door on all fifty. Meanwhile, the real troublemaker just walks around the block and comes back from a different street each time, and the bouncer treats him like a brand-new guest every single time. A smarter bouncer stops looking at the street entirely. He checks the ID card in your hand — because that stays the same no matter which door you walk through.
In software, that "ID card" is one of three things: an API key (like a secret password given to an app), a User ID (the account someone logged into), or a session cookie (a little tag your browser keeps that says "this is still the same visit as before"). Unlike an IP address, you handed these out yourself, they're hard to fake, and they stay attached to the same real visitor no matter which network or device they use.
3. The Fix — Limiting by Who Someone Is
The fix is simple to say, even if it takes some building: before you start counting anything, first figure out who is actually making the request. Only after that do you start counting. The actual counting method barely changes — what changes is what you're counting against.
3.1 Why Counting By "Who" Actually Works
A rate limiter is really just a little counter attached to a label, with a time window ("in the last minute") and a max number allowed. When the label is an IP address, you're counting "how much traffic came from this location." When the label is an API key or User ID, you're counting "how much traffic came from this account" — and that's the thing you actually care about.
This also becomes fairer. That office building with two hundred employees sharing one IP address? Each employee now gets counted separately, because you're recognizing two hundred different accounts, not one shared street address.
3.2 Trade-off — API Key vs User ID vs Cookie
These three "ID cards" aren't interchangeable — pick based on who's actually visiting:
API Key — best when another computer program or app is calling you (not a human clicking a browser). It's strong, you can turn it off if it leaks, and you can give different customers bigger or smaller limits based on what they paid for. The one risk: if someone steals the key, they get to use that quota too.
User ID — best once someone has actually logged in. It sticks to their account no matter what device, network, or IP address they switch to next, and it's easy to ban a specific troublemaker's account.
Session Cookie — best for visitors who haven't logged in yet, like someone just browsing or signing up. It's the weakest of the three, since a cookie can be deleted, so it's smart to also glance at the IP address as a backup signal here.
4. Old Way vs New Way, Side by Side
🚫 Old Way: By IP Address
Punishes whole buildings/schools for one troublemaker
Easy to dodge by switching addresses
Can't tell a paying customer from a free one
Breaks when phones change addresses on their own
✅ New Way: By Who You Are
Every account gets its own fair share, no matter the network
Switching addresses doesn't reset the count anymore
Easy to give bigger limits to paying customers
Keeps working even as devices and networks change
5. Where This Fits in Your App
Think of a request traveling through your app like it's passing through a few checkpoints in a row, one after another:
⬇️
⬇️
⬇️
⬇️
6. Step-by-Step: Building It
① Decide your order of preference for recognizing visitors: API key first, then logged-in User ID, then cookie, and IP address only as a very last backup.
⬇️
② Figure out who the visitor is as early as possible, before your app does any real work.
⬇️
③ Turn that identity into one simple text label, like user:123, instead of using the raw IP address.
⬇️
④ Keep the counters in one shared, fast storage place (a tool called Redis is the common choice) so the limit works correctly even if your app is running on several servers at once.
⬇️
⑤ When someone goes over their limit, politely tell them "try again in X seconds" instead of just silently refusing.
⬇️
⑥ Keep a record of who gets blocked, sorted by their identity label — not their IP — so you can actually see which accounts are misbehaving.
7. The Code
Think of this code as a little doorman that runs before every single request. First, it looks for the best way to recognize the visitor — an API key, then a User ID, then a cookie, then finally the IP address if nothing else is available. Then it uses that label to keep a simple counter in a fast storage tool called Redis (a place to store small pieces of data very quickly, shared across all your servers). If the counter goes above 100 for that person within one minute, it says "slow down" and tells them exactly how many seconds to wait. This solves the exact problem from earlier: no matter how many different addresses someone switches between, the counter still recognizes them as the same visitor.
const redis = require('./redisClient');
const WINDOW_SECONDS = 60; // the time window we're counting in (1 minute)
const MAX_REQUESTS = 100; // how many requests are allowed in that window
// Step 1: figure out WHO this visitor really is
function resolveIdentity(req) {
if (req.headers['x-api-key']) return `apikey:${req.headers['x-api-key']}`;
if (req.user && req.user.id) return `user:${req.user.id}`;
if (req.cookies && req.cookies.sid) return `session:${req.cookies.sid}`;
return `ip:${req.ip}`; // only used if nothing else is available
}
// Step 2: count their requests, and decide if they've had too many
async function rateLimit(req, res, next) {
const identity = resolveIdentity(req);
const key = `ratelimit:${identity}`;
const current = await redis.incr(key); // add 1 to their counter
if (current === 1) {
await redis.expire(key, WINDOW_SECONDS); // start a fresh 1-minute timer
}
if (current > MAX_REQUESTS) {
const secondsLeft = await redis.ttl(key); // how long until they can try again
res.set('Retry-After', secondsLeft);
return res.status(429).json({ error: 'Too many requests', identity });
}
next(); // they're still within their limit, let the request continue
}
module.exports = rateLimit;
Look closely: the counting part (adding 1, checking a max, starting a timer) is exactly the same simple idea as before. The only thing that actually changed is resolveIdentity() — the one small function that decides who to count. That's the entire difference between a rate limiter that attackers laugh at, and one that actually holds up.
8. How Do You Know It's Actually Working?
Test it with realistic, mixed traffic. Try sending lots of traffic from many different accounts behind one shared IP (like the office scenario) and confirm none of them get blocked unfairly. Then try sending traffic from one troublemaker jumping between many IPs, and confirm they still get blocked.
Break things on purpose. Turn off your Redis storage in the middle of testing and see what happens. It should fail safely — either quietly falling back to a simpler backup limit, or clearly alerting you — never crashing the whole app.
Check who's actually getting blocked. If most of your blocked requests are still coming from the "IP address" fallback instead of an API key, User ID, or cookie, that's a sign your "who is this?" step is missing something and needs a closer look.
9. What's New
Checking closer to the visitor. More companies are now doing this "who are you, and are you over your limit?" check right at the edge of their network (the very first server a request touches), instead of waiting until it reaches the main app. This blocks bad traffic faster and closer to the source.
Smarter, flexible limits. Instead of one fixed number for everyone, some systems now give a brand-new, unverified visitor a tighter limit, and a long-trusted, verified account a more generous one.
Smoother counting methods. Older "fixed window" counting (resetting sharply every minute) let people sneak in a burst of extra requests right at the reset moment. Newer, smoother counting methods spread the check out more evenly, closing that loophole.
Built into off-the-shelf tools. Many companies no longer build this from scratch — API gateways (the front-door software that manages all incoming traffic) increasingly come with "recognize the visitor, then limit them" built in as a ready-made setting.
FAQ
Should I stop using IP-based limiting entirely?
No — keep it as a cheap, simple outer layer, especially for visitors who haven't logged in yet (like on a login page). Just don't rely on it as your only defense; layer it underneath identity-based limiting.
What if a visitor isn't logged in yet?
Give them a signed cookie the first time they visit. It's not as strong as a real account, but it's far more reliable than an IP address, and it sticks around across their whole visit.
Can someone just fake an API key or User ID?
Not easily, if you generate and check them properly on your server. That's exactly why this method is stronger — faking a real credential is much harder than simply switching to a different IP address.
Where should the counters be stored?
In one shared, fast storage tool (like Redis) that all your servers can read from together — otherwise each server would keep its own separate count, and the limit wouldn't actually work correctly.
Does this slow requests down a lot?
A well-built version usually adds only a couple of milliseconds — barely noticeable. If it's adding a lot more than that, it's a sign to check your storage setup, not a reason to give up on this approach.
What's the difference between rate limiting and throttling?
Rate limiting flat-out refuses requests once someone goes over the limit. Throttling just slows them down instead of refusing them outright. Many real systems use both together — slow down first, and only refuse as a last resort.
Final Summary
🌐 An IP address tells you which door a request came through — it doesn't tell you who's actually standing there.
🔑 Always figure out who someone is first — API key, User ID, or cookie — before you even start counting their requests.
🧮 The counting itself barely changes — what you count against is what actually makes it work in real life.
📊 Test with realistic mixes of visitors, not just a flood of traffic, and keep watching how often people get blocked and how much time the check adds.
Happy Building! 🚀🔥
Comments
Post a Comment