The Bug: Per-IP Limiting Doesn't Protect Shared Quotas
I capped my /image endpoint at 3 requests per minute per IP using express-rate-limit. Standard practice. Felt responsible. Then I realized my endpoint calls two third-party image generation providers with account-wide limits: Provider A allows 5 images per minute, Provider B allows 10.
Five different users on five different IPs can each stay under their personal 3/min cap while collectively sending 15 requests per minute to a provider that only allows 5. Every request returns success from my server, then silently fails somewhere between my server and the provider. The failure is invisible to both me and the user.
Why Default Rate Limiting Fails
Rate limiting by IP assumes the resource you're protecting has infinite capacity. That assumption breaks when your endpoint relies on a third-party API with per-account quotas. The bug doesn't appear in testing with one user. It appears exactly when you have enough concurrent users for their individual well-behaved usage to sum to abuse.
The Fix: Shared Counter Before Provider Calls
Instead of per-IP buckets, maintain a shared log of requests to each provider, checked before every call:
const providerRequestLog = {
providerA: [],
providerB: []
};
const PROVIDER_LIMITS = {
providerA: 5, // per minute, account-wide
providerB: 10
};
function checkCapacity(provider) { const now = Date.now(); const windowStart = now - 60_000; const log = providerRequestLog[provider];
while (log.length > 0 && log[0] < windowStart) { log.shift(); }
if (log.length >= PROVIDER_LIMITS[provider]) { return { allowed: false }; }
log.push(now); return { allowed: true }; }
Before calling the provider, check capacity against the shared log. If no room, reject with a clear `Retry-After` header instead of discovering the failure from a provider 429 you didn't see coming.
## Caveat: In-Memory Only Works on Single Process
This implementation stores the log in memory. If you run multiple instances behind a load balancer, each instance enforces its own version of a limit meant to be global. Use Redis or a similar shared store for multi-process deployments.
## The Real Lesson
Rate limiting isn't one problem. "Protect against a single abusive user" and "don't exceed a shared ceiling across all users" are two different problems that look identical in a middleware config. Default setups solve only the first. If your backend depends on any third-party API with account-wide quotas—payment, AI, anything with per-project limits—check that your rate limiting reflects the shared ceiling, not just per-IP politeness.


