Why Your API Rate Limiter Is Costing You Real Traffic
Misconfigured rate limits don't just reject spam. They reject paying customers during traffic spikes, silently kill retries, and hide themselves behind generic 429 errors. Here's what actually happens when you measure it.
· 7 min read
You've Silently Rejected Your Customer Without Knowing It
Your API rate limiter worked exactly as designed yesterday. It throttled a burst of requests in 47 milliseconds. Your dashboard showed green. But somewhere across that traffic, a customer's checkout abandoned because their mobile app hit 429 after the third retry. The app didn't log it as a problem. Your monitoring didn't alert on it. Your rate limiter looked at a request limit per second and decided the answer was no.
This happens every day in production systems everywhere. Not because rate limiting is broken. Because the people building it never measured what "working as designed" actually costs.
What a 429 Really Does (And Why You're Blind to Half of It)
A rate limit rejection is not a discrete event. It's the start of a cascade.
The immediate rejection is obvious: request denied, HTTP 429, Retry-After header sent. The customer sees a spinner. Or they don't, and the app just fails.
What happens next depends on the client. HTTP-compliant clients read Retry-After and wait. Poorly-written clients retry immediately. Mobile apps might queue the request. Browser tabs might drop it entirely. A service calling your endpoint might have a 5-second timeout and give up. If it was part of a transaction, your database now has a locked row that nobody finished paying for.
The cost isn't in the request you rejected. It's in the silence around why.
Take a checkout flow. Customer adds item to cart (1 request). Applies discount code (1 request). Views shipping options (2 requests). Enters payment method (1 request). Completes payment (1 request). That's 6 requests in maybe 8 seconds on a slow phone. If your rate limit is set to 2 requests per second per user, the payment request gets rejected. The customer doesn't know if it's a network error or the API. They refresh. They try again. The second attempt hits the same limit. Most abandon.
You have no alert for this. Your monitoring shows requests/sec is normal. Your error rate looks fine because 429s often aren't counted in the same dashboard as 500s. You wake up to a Slack message about "checkout conversion dropped 8%" without any technical signal pointing you toward the rate limiter.
The Metrics Your Dashboards Don't Show
You're probably monitoring:
- Requests per second (total)
- Error rate (usually filtered to 5xx)
- p99 latency
- Maybe 429 count
You're almost certainly not monitoring:
- 429s as a percentage of total traffic (not just count)
- Retry behaviour after a 429 (do retries succeed, or do they hit the limit again?)
- The origin of rejected requests (same user repeatedly? A specific client? A partner integration?)
- Time-to-resolution (how long until the same user/client succeeds after a 429?)
- The cost of a failed checkout vs. a successful 429 rejection
That last one matters most. If your rate limiter rejects 0.3% of traffic, and 40% of that traffic is checkout requests, and your conversion value is RM200 per checkout, and 60% of rejected checkouts don't retry: you're leaving RM48 per hour on the table. Multiply that by business hours. Month it. That's real money.
Most teams have never calculated this number.
How to Measure the Actual Damage
Start here. You don't need fancy tooling.
Step 1: Measure what you're rejecting, and from whom
Add a metric: api.rate_limit.rejected_requests. Tag it by:
- User ID (or a hash of it, for privacy)
- Client identifier (your mobile app version, partner name, service name)
- Endpoint
- Time of day
You want to see patterns. Are rejections clustered around a specific user, a specific partner, or a specific time window? If it's a partner integration spamming your endpoint, that's one problem. If it's your own mobile app during peak hours, that's a different problem. If it's evenly distributed, your limit might be too tight globally.
Step 2: Track what happens after the 429
Instrument your rate limiter to emit a metric when it rejects a request, then look for a retry from the same (user_id, endpoint, client) tuple within 2 seconds. Tag the outcome: success, rejected_again, timeout, or not_retried.
This tells you whether your rate limit is a speed bump (requests succeed on retry) or a wall (requests fail permanently).
A 60% success-on-retry rate suggests your limit is proportional to legitimate traffic and clients handle backoff correctly. A 20% success-on-retry rate suggests either your limit is too tight, or your clients aren't reading Retry-After headers.
Step 3: Measure the type of request being rejected
Not all requests are equal. A rejected health check is noise. A rejected payment authorization is revenue loss. A rejected analytics ping is a data loss. Add context to your metric:
api.rate_limit.rejected {
user_id: "user_12345",
endpoint: "/checkout/payment",
is_transactional: true,
is_idempotent: false,
business_impact: "high"
}
If you're rejecting 100 requests per minute but they're all from health checks or retries, you're not losing money. If you're rejecting 10 per minute and half are transactional, you are.
Step 4: Connect the 429 data to business metrics
Pull data for the same hour from two sources: your rate limiter logs and your analytics/billing system. Compare:
- Hour with X 429s on checkout: average order value, checkout completion rate
- Hour with no 429s on checkout: average order value, checkout completion rate
If checkout 429s drop 0.4% and conversion rate drops 0.3%, there's your correlation. It's not proof. But it's enough to justify fixing the limit.
The Usual Mistakes (And Why They're Expensive)
Mistake 1: A single global rate limit
Setting a single limit like "1000 requests per second per user" treats all users as identical. A mobile app in traffic might legitimately send 10 requests per second during a search. A backend batch job might average 2 per second. A health check might be 1 per minute.
The right approach: different limits by client type. Your mobile app gets one budget. Your batch job gets another. Your partner integration gets a third. You'll need to know who's calling you, which means adding client identification (API key, JWT claim, user-agent) to your rate limiter decision.
This requires logging and observability, so you have to know who's hitting the limit before it hurts.
Mistake 2: Rejecting instead of queuing
A rate limiter doesn't have to reject immediately. It can queue. If your API can handle bursts internally (via a background queue system like the one we built for a multi-tenant EV-charging platform), rejecting is optional. You can accept the request, return a 202 Accepted, process it asynchronously, and store the result.
The customer gets a response instantly. The backend handles the load smoothly. No 429.
This only works if the operation is truly asynchronous (most checkout flows aren't, but status checks, notifications, and analytics are). It also means more infrastructure. But for high-value requests, it's cheaper than losing the transaction.
Mistake 3: Setting the limit based on what your server can handle, not what your customer needs
This is the backwards approach. You measure your server's max throughput (say, 5000 requests per second), divide by expected concurrent users (say, 10000), and set a limit of 1 request per 2 seconds per user.
This breaks the moment actual users don't match your model. If you have 20000 concurrent users instead of 10000, everyone gets rejected. If 80% of users are inactive and 20% are active, your active users starve.
The better approach: measure what your actual customers need, then measure your server, then find the intersection. A customer browsing products might need 1 request per second during search. A paying customer checking out might need 8 per second for 10 seconds. A batch import might need 100 per second for 30 seconds, but only during off-peak.
Design limits around customer behavior, not server capacity. Then autoscale your server if the limit is too tight.
Mistake 4: No Retry-After header, or no retry logic
If you reject a request with HTTP 429 but no Retry-After header, the client has to guess how long to wait. Most will retry immediately, hit the limit again, and give up. With Retry-After: seconds 5, the client knows to wait 5 seconds and retry. The retry usually succeeds because the window has reset.
Your rate limiter should emit Retry-After on every 429. Your clients should read it. Most clients do for HTTP. Most mobile apps don't. If you control the mobile app, fix it. If you don't, you're exposing your backend to retry storms.
Mistake 5: Rate limiting downstream of your actual bottleneck
Your rate limiter sits at the API gateway. It rejects requests. But what if the actual bottleneck is your database, which can only handle 500 queries per second, not 5000?
Rate limiting the requests is correct. But limiting at the wrong value is expensive. If you limit at 2000 requests per second and your database can only do 500, half your traffic gets rejected before it even reaches the bottleneck. Customers experience failure. Your database sits partly idle.
The right approach: monitor your database queue depth, query latency, and error rate. Set your API limit to a value that keeps database latency below a threshold (say, p99 latency stays under 200ms). When that threshold breaks, the rate limiter tightens. This is dynamic rate limiting, and it's harder to implement but it keeps customers out of the weeds.
What to Do Right Now
Audit your rate limiter config. For each endpoint, answer these questions:
- What's the current limit? (Per user? Per API key? Global?)
- Why is it set to that value? (Server capacity? Business decision? Someone's guess three years ago?)
- How many requests actually hit that limit per day?
- Of those rejected requests, what percentage are transactional (checkout, payment, data mutation)?
- Of transactional rejections, what percentage are retried successfully within 5 seconds?
If you can't answer these questions, your rate limiting is misconfigured. You just don't know it yet.
Create a dashboard: [Time series chart: 429 count by endpoint]. [Scatter plot: 429 count vs. checkout conversion rate, by hour]. [List: top 10 users getting rejected most, and how many].
Run this for a week. Share it with your product and finance teams. Ask them: "How much revenue are we leaving on the table because of these rejections?"
If the answer is "none," your limits are probably correct. If it's more than RM500 per week, fix the limiter. The ROI is immediate.
Rate limiting exists to protect your service. But protection that costs you more than it saves isn't protection. It's overhead. Measure it, and fix it.
Keep reading
- sreobservability
Why Your Error Budget Is Lying to You (And How Observability Actually Fixes It)
Error budgets sound clean in theory but fail silently when you can't see where the unreliability actually is. Here's how to build observability that makes them real.
- inventory managementsupply chain
When Your Inventory Math Doesn't Match What's Actually in the Warehouse: How Rounding Errors and Manual Updates Turn Small Problems Into Supply Chain Disasters
Small inventory errors compound fast. Rounding mistakes, manual data entry delays, and systems that don't talk to each other create cascading supply chain failures that cost more to fix than the system that would prevent them.
- operationscost control
Why Your Vendor Lock-In Isn't Just a Tech Problem—It's Bleeding Your Operating Budget
Vendor lock-in costs most businesses real money every month: wasted time, duplicated effort, and the panic bill when you finally want to leave. Here's what it actually costs, and how to spot it before you're trapped.