I want to talk about something that sounds boring on the surface but is one of the easiest ways to take down an entire API: lack of resources and rate limiting. This is officially known in the OWASP API Security Top 10 as API4:2023 – Unrestricted Resource Consumption, and I have seen it break production systems more times than I can count.
If you build, secure, or test APIs, this is a topic you cannot skip. Let me break it down the way I wish someone had explained it to me the first time.
What “Lack of Resources and Rate Limiting” Actually Means
Every request that hits an API costs something. It costs CPU time, memory, database connections, bandwidth, and sometimes money (think of API calls to a paid third-party service like an SMS gateway or an AI model). When an API does not put a limit on how many requests a client can make, or how large a request can be, or how long a request can run, an attacker — or even just a careless user — can consume all of that capacity.
The result is simple: the server slows down, runs out of memory, hits its database connection pool limit, or racks up a massive bill. This is called resource exhaustion, and when it is done on purpose, it is called a Denial of Service (DoS) attack.
I like to think of it like a restaurant with no reservation system and no limit on how many dishes one table can order. One customer could order 500 plates of food, tie up the entire kitchen, and every other customer would starve while waiting.
Why This Happens: The Root Causes
Let me walk through the actual reasons this vulnerability shows up in real APIs.
1. No Rate Limiting on Endpoints
This is the most common cause. A login endpoint, a search endpoint, a password reset endpoint, or a report-generation endpoint accepts unlimited requests per second from a single IP address, user account, or API key.
2. No Limit on Payload Size
An API accepts a JSON body, a file upload, or an array in a request without checking its size. Someone sends a 2GB JSON file, or an array with 10 million items, and the server tries to parse the whole thing into memory at once.
3. No Limit on Response Size or Pagination
An endpoint like /api/users returns every single user in the database in one response instead of paginating results. If there are 5 million users, that single request can crash the server or the client.
4. Expensive Operations Without Throttling
Some endpoints are naturally expensive — generating a PDF report, running a complex database query with joins, resizing an image, or calling a third-party AI model. If these are not throttled per user, one person can call them repeatedly and burn through server resources or your budget.
5. No Timeout on Long-Running Requests
If a request can run forever — for example, a regex that takes exponential time to evaluate (a classic ReDoS, Regular Expression Denial of Service) — the server thread or process gets stuck, and enough stuck threads mean the whole server stops responding.
6. Missing Limits on Array or Batch Operations
Many APIs allow batch operations like POST /orders/bulk where you can submit multiple orders in one call. Without a cap on how many items can be in that batch, one request can effectively become thousands of database writes.
7. No Cost Controls on Third-Party API Usage
If your backend calls an external paid API (like a mapping service, an AI model, or an SMS provider) on behalf of a user, and there’s no per-user quota, an attacker can spam your endpoint and drain your budget in minutes. This is sometimes called a Denial of Wallet attack.
Real-World Style Example
Imagine a food delivery API with this endpoint:
POST /api/v1/restaurants/search
{
"latitude": 33.6,
"longitude": 73.0,
"radius_km": 5000
}
Nothing stops the client from setting radius_km to 5000 instead of 5. Now the query has to scan almost the entire restaurants table. If ten thousand attackers send this same request at once, the database grinds to a halt, and legitimate customers cannot even open the app.
Another classic case: a /api/export-report endpoint that generates a PDF of a user’s entire transaction history. If it has no rate limit, an attacker can hit it 200 times per second, and the server keeps spinning up 200 PDF-generation processes simultaneously, consuming all available memory.
The Many Faces of Resource Exhaustion Attacks
I want to break this vulnerability further into all the sub-attack types that fall under this umbrella, because “rate limiting” is really an umbrella term.
Volumetric Attacks
Simply sending a huge number of requests in a short time to overwhelm the server. This is the most basic form of DoS.
Slow Request Attacks (Slowloris style)
Instead of sending many requests fast, the attacker opens many connections and sends data very slowly, keeping connections open and exhausting the server’s connection pool.
Large Payload Attacks
Sending oversized JSON bodies, huge file uploads, or deeply nested JSON objects (which can cause a stack overflow when parsed recursively).
Algorithmic Complexity Attacks
Exploiting an endpoint whose processing time grows exponentially with input size — like a poorly written regex, or a sorting algorithm with a worst-case scenario triggered by crafted input.
Resource-Intensive Query Attacks
Abusing search, filter, or reporting endpoints that allow flexible queries (like GraphQL) to request deeply nested or extremely broad data in one call.
Brute Force Attacks
Rate limiting isn’t just about server load — it’s also about security. Without rate limiting on a login or OTP verification endpoint, attackers can try thousands of password or OTP combinations per second.
Distributed Resource Exhaustion
Instead of one IP hammering an endpoint, an attacker uses thousands of IPs (a botnet) to spread the requests so IP-based rate limiting doesn’t catch it.
How to Detect This Vulnerability
When I test an API for this issue, here’s my checklist:
- Send rapid repeated requests to the same endpoint and check if you ever get throttled (HTTP 429 Too Many Requests).
- Check response headers for rate-limit indicators like
X-RateLimit-Limit,X-RateLimit-Remaining, orRetry-After. Their absence is often a red flag. - Send an oversized payload — a huge JSON body or a large file — and see if it’s rejected before being processed.
- Try pagination bypass — request a huge
limitorpage_sizeparameter and see if the API just returns everything. - Test batch endpoints with thousands of items in one array.
- Check for timeouts — send a request designed to take a long time and see if the server ever cuts it off.
- Look at authentication endpoints specifically — login, signup, OTP, and password reset are prime brute-force targets.
How to Fix It: Practical Prevention Techniques
Now let’s get into how I would actually fix this in a real system.
1. Implement Rate Limiting at Multiple Levels
- Per IP address — basic but easily bypassed with proxies.
- Per user account / API key — much more reliable.
- Per endpoint — sensitive endpoints like login need stricter limits than a public product listing endpoint.
Common algorithms:
- Fixed Window — simple counter reset every X seconds.
- Sliding Window — smoother, avoids bursts right at window boundaries.
- Token Bucket — allows small bursts but enforces an average rate over time.
- Leaky Bucket — similar to token bucket but smooths output more strictly.
2. Set Maximum Payload Sizes
Configure your web server (Nginx, API gateway, or framework middleware) to reject requests above a certain size before your application code even touches them.
3. Enforce Pagination
Never allow unbounded GET list endpoints. Always cap limit/page_size to a sane maximum (e.g., 100) regardless of what the client requests.
4. Add Timeouts Everywhere
Set timeouts on database queries, external API calls, and the overall request lifecycle. A request that takes too long should be killed, not left running forever.
5. Limit Batch Operation Sizes
If your API supports bulk creation or bulk updates, cap the number of items per request (e.g., max 50 items per batch call).
6. Use an API Gateway
Tools like Kong, AWS API Gateway, Apigee, or NGINX with rate-limiting modules can enforce limits before traffic even reaches your backend.
7. Apply Quotas for Costly Operations
For anything that calls a paid third-party service or does heavy computation, track usage per user and enforce a daily/monthly quota, not just a per-second rate limit.
8. Protect Against ReDoS
Avoid writing regex patterns vulnerable to catastrophic backtracking. Use regex engines with timeout protection, or validate input length before running regex on it.
9. Add CAPTCHA or Progressive Delays on Sensitive Endpoints
For login and OTP endpoints, after a few failed attempts, introduce increasing delays or require a CAPTCHA.
10. Monitor and Alert
Set up monitoring for sudden spikes in traffic, error rates, or resource usage so you can react before a full outage happens.
A Simple Rate Limiting Example (Conceptual)
Here’s a very simplified idea of a token bucket check in pseudocode, just to show the logic:
function isAllowed(userId):
bucket = getBucket(userId)
refillBucket(bucket) // add tokens based on time passed
if bucket.tokens >= 1:
bucket.tokens -= 1
return true
else:
return false // reject with 429 Too Many Requests
This kind of logic, combined with a fast in-memory store like Redis, is how most production rate limiters work at scale.
Business Impact If Ignored
- Downtime — your whole service becomes unavailable for real users.
- Financial loss — if you pay per API call to a third-party service, an attacker can generate a massive bill.
- Reputation damage — customers lose trust in a service that frequently goes down.
- Security risk multiplier — lack of rate limiting on login/OTP endpoints directly enables account takeover through brute force.
Final Thoughts
Rate limiting and resource control are not optional extras — they are core infrastructure for any API that faces the public internet. I always tell people: assume every endpoint you expose will eventually be hit by an automated script sending thousands of requests per second, because eventually, it will be. Build your defenses before that day comes, not after.