August 22, 2026
Why Rate Limiting Is Not Enough Against DoS Attacks
Three kinds of DoS vulnerability, and why rate limiting is not enough.
By ahmad kabiri
4 min read
Three kinds of DoS vulnerability, and why rate limiting is not enough.
Rate limiting reduces how often an attacker can hit an endpoint. But DoS depends on something else too:
System Load ≈ Request Rate × Cost per Request
A rate limiter controls Request Rate.
It does not necessarily control Cost per Request.
DoS vulnerabilities are divided into three categories: Resource-Exhaustion DoS, Crash DoS, and Algorithmic-Complexity DoS. In Resource-Exhaustion, an attacker sends a lot of requests to a target to increase the processing, storage or network consumption. Usually, this kind of DoS is conducted by distributed systems and is called a DDoS attack. This kind of DoS can be mitigated by:
- Timeouts at Every Boundary — Application / DB / Network Layer — Define an explicit deadline on client read, client write, upstream call, DB statement and connection acquisition. A request with no deadline can holds a worker, a socket and a pool slot indefinitely, so occupancy can grows even at fixed entry rate. With a deadline, worst-case occupancy becomes
rate × timeout— a number you can actually size capacity against. most HTTP clients provide no read timeout at all. - Admission Control & Bounded Queue & Load Shedding — Application Layer — Cover the queue and return 503 (with
Retry-After message) when it's full, rather than letting the backlog grow. An unbounded queue turns the overload into increasing latency and memory until everything fails all together; a bounded one keeps the accepted subset healthy and fast. one point; use priority-based dropout: drop anonymous and background traffic before authenticated interactive traffic. - Concurrent-Request Limits — Application Layer — Bound in-flight work per endpoint, not just the rate of arrivals. Rate limiting counts arrivals over a window; a concurrency cap bounds simultaneous execution, which is what actually consumes threads, memory and pool connections. This is the control that still holds when each request is expensive, specially in the cases where a rate limiter under-measures real load.
- Connection Limits & Slow Read/Write Timeouts — Load Balancer / Web Server Layer — Limit concurrent connections per source, and Disconnect clients that send data too slowly. Slowloris-class attacks stay far below any request-rate threshold because they never complete a request; they exhaust the connection table instead. Set
client_header_timeout,client_body_timeoutand minimum throughput rate, not only request-level timeouts. - Identity-Based Quotas (User / API Key / Tenant) — Gateway / Application Layer — Apply limits mainly per authenticated user or account, and use IP only as a rough filter before login. An IP address does not reliably identify a single user (CGNAT, corporate egress, shared proxies) nor scarce (IPv6 /64s, residential proxy pools), so IP-only limits simultaneously block legitimate users and miss distributed attackers. We should check the quota limit before doing the expensive work, not after that.
- Edge Absorption & Response Caching — Edge Layer — Let the CDN handle incoming connections and serve cached responses so requests do not reach the origin server. This is the only control that fixes the capacity asymmetry. Make sure attackers cannot bypass the cache just by changing query parameters.
But there is another DoS vulnerability called Crash DoS (or Software-Triggered DoS). This vulnerability is created by a bug in the application that leads to an endless loop, unhandled exception, OOM kill, or an exponential increase in the number of processes. In these scenarios, request numbers are usually small and usually one request is often enough! So, rate limiting cannot protect and be useful. The rate stays under any threshold and limiter never sees enough volume to trigger; because the damage is in Cost per Request, which the limiter does not measure. this kind of attack cab be mitigated by these solutions:
- Dependency Patching Pipeline — SDLC Layer — Know which dependencies you use and update vulnerable ones quickly. In practice, most exploitable crash-DoS is a published CVE in a third-party parser, image library, decompressor or serializer — not in your own code.
- Schema-Based Input Validation — Application Boundary — Validate type, length, range and structure with an allow-list before the input reaches parsing or business logic.
- Execution / Recursion / Iteration Limits — Code / Runtime Layer — Limit input-driven loops and recursion, and stop processing if it takes too long.
The third type of DoS is called Algorithmic-Complexity (ReDoS, zip bomb, hash-collision flooding, deeply nested JSON/GraphQL queries). It has a high Cost per Request and no crash. In fact, the code behaves exactly as designed but a small input can lead to a large computation. This kind of DoS can be mitigated by:
- Non-Backtracking Regex & Randomized Hashing — Code / Runtime Layer — Replace backtracking regex engines with RE2 or Rust
regexfor any pattern touching user input, and use a runtime with randomized hashing (SipHash) for input-keyed maps. - Persisted Queries — API Layer — Register and hash approved GraphQL operations, and reject anything not on the list; disable introspection in production. This removes the attacker's ability to author the computation at all, which is strictly stronger than trying to price arbitrary queries.
- Pre-Execution Complexity Budget with Field Cost Weighting — API / Application Layer — Estimate the query cost before running it: consider field costs, pagination size, and total nodes, then reject queries that are too expensive. Depth limits alone are not enough— a two-level query with
first: 10000on a nested list is shallow and extremely expensive. - Input Size & Nesting Depth & Compression Ratio Caps — Gateway / Parser Layer — Bound body size, JSON/XML nesting depth, array and field counts, and abort decompression on ratio or output-byte breach while streaming. The ratio cap is the one people miss: a size limit still admits a 1 MB archive that expands to gigabytes. In XML parsers, also disable DTD processing and entity expansion.
- Execution Timeout With Real Cancellation — Application / Database Layer — Propagate a cancellation context through the call chain and set
statement_timeouton the database. A timeout that only abandons the HTTP response leaves the query running and the cost fully paid; the client disconnects, the attacker sends the next request, and the load compounds invisibly. - Mandatory Pagination & Result and Scan Caps — API / Database Layer — Require a bounded
limitwith a server-enforced maximum, reject filters and sorts on non-indexed columns, and cap rows scanned. Unbounded list endpoints and free-form filtering are the highest-yield algorithmic-complexity targets in ordinary REST APIs — no GraphQL or regex needed. - Per-User Resource Budget (CPU-ms / Complexity Points) — Application Layer — Meter consumed cost, not request count, and throttle the identity when the budget is spent. This is the direct implementation of
Load ≈ Rate × Cost per Request: it prices the second factor, which every rate-based control ignores by construction.