Why Rate Limiting Isn't Enough: Designing APIs to Survive Application-Layer DoS

Availability is the least appreciated pillar of the CIA triad, until it disappears.

Most engineers picture a Denial of Service attack as a flood of malformed packets hitting network infrastructure. The usual defense follows that picture: a CDN, DDoS protection, some rate limiting. Those controls matter. They also solve a problem this article isn't about.

Here's the scenario I want to walk through instead: a mobile money platform, millions of users, and a payment initiation endpoint that never got hit by a single malformed packet, and still nearly went down.

Expensive Traffic

Picture the platform on an ordinary day. Customers initiate transfers, pay bills, check balances, buy airtime. Each request travels through a chain of microservices before a response comes back. The infrastructure scales the way it's supposed to. Monitoring dashboards stay green.

Then someone starts asking the API to do exactly what it was built to do. Over and over and over again.

A More Realistic Attack

The attacker doesn't start with an exploit. Days of reconnaissance come first, aimed at the payment initiation endpoint specifically: watching how it behaves across different regions, different API gateways, different authentication flows. No vulnerability scanning, no password brute forcing. Just observation.

Eventually a pattern surfaces. The platform is globally distributed. Requests through one regional gateway hit strict throttling. Requests through another gateway meet a slightly different limit. Neither gateway looks vulnerable on its own. Together, they form an inconsistency, and that inconsistency is the opening. This is an architectural weakness, not a bug in anyone's code.

Weaponizing Legitimate Requests

The attacker skips obviously malicious traffic entirely and spreads requests across thousands of compromised devices instead. Every one of those requests carries a valid session token, follows the documented API contract, contains syntactically correct JSON, and targets the real payment endpoint.

Nothing about any single request looks suspicious. Traditional signature-based detection has nothing to flag, because nothing here is malformed. The platform sees authenticated users asking to initiate transactions, which is the one thing the payment endpoint exists to do.

That's the mechanism that makes application-layer attacks dangerous on a system like this one. The attacker isn't going around the business logic. The attacker is going straight through it.

Why Authentication Doesn't Save You

Plenty of engineering teams assume authentication filters out attackers before they ever reach the interesting parts of the system. On this platform, it didn't, because the attacker never showed up anonymously.

Credential stuffing against previously leaked username and password combinations gave the attacker a working set of real accounts. Only a small percentage of attempts needed to succeed. That was enough to turn every subsequent request into one carrying a genuinely valid identity.

Authentication succeeds. Authorization succeeds. Request validation succeeds. The platform has no reason built into it to reject any of this traffic. The very controls meant to separate real users from attackers end up laundering the attacker into the "legitimate" bucket.

The Real Target Isn't the API Gateway

The obvious response at this point is "rate limit the endpoint harder." That misses what the attacker is actually going after.

Nobody was trying to overwhelm nginx, the load balancer, or Cloudflare. The target sits behind all three. Every payment initiation request on this platform touches worker threads, database connections, cache lookups, fraud detection rules, ledger preparation, and a chain of downstream service calls. One request costs almost nothing. A million of them cost everything.

The payment processing service eventually hits thread exhaustion. Incoming requests start queuing. Timeouts climb. Retries multiply. Here's the twist: legitimate customers start adding to the load themselves, hammering Retry after every failed transfer. The attack starts feeding itself.

When Success Becomes Failure

Cloud-native systems are built to scale under load, and that's exactly what worked against this platform. More requests trigger more containers. More containers trigger more compute. On paper, problem solved.

Autoscaling assumes that more traffic means more real business. The attacker knew that assumption going in, and never pushed hard enough to crash anything outright. Just enough sustained load to keep autoscaling running indefinitely.

The service stayed technically available. Customers kept getting responses. Infrastructure monitoring kept reporting healthy compute utilization. Underneath that, cloud costs climbed fast, worker pools stayed saturated, database pressure kept building, and real transaction latency kept getting worse.

Availability never fully disappeared here. It just got more expensive by the minute. The goal was never to destroy the platform. It was to make running the platform economically unsustainable.

Rate Limiting Is Necessary, Not Sufficient

Rate limiting is still one of the better first-line defenses available, and it's also the reason this platform's gateways felt safe right up until they weren't. The mistake was treating it as the only line of defense.

Rate limits on this platform operated per IP, per API key, per user, per device, each on its own. The attacker spread requests across thousands of identities, and every individual request slipped in comfortably under whatever threshold applied to it. The aggregate impact was the part nobody was measuring.

Worse, the distributed gateways enforced these limits inconsistently. One gateway ran strict controls. Another regional deployment ran a slightly looser threshold. A third cached its counters on a different schedule entirely. All the attacker needed was one gap between them. Security controls are only as strong as the weakest gateway enforcing them.

Designing APIs That Expect Abuse

The question that would have actually protected this platform isn't "how do we stop too many requests." It's "how do we stop expensive requests."

Cheap validation needs to happen before expensive work starts. Authentication checks belong ahead of database queries wherever that's possible. Malformed requests should never make it as far as business logic. Idempotency has to prevent the same payment request from executing twice. Circuit breakers on critical downstream services need to refuse unlimited workloads rather than absorb them.

Rate limiting itself needs to exist at every layer: edge networks, API gateways, service meshes, and the individual business services underneath them, with each layer built to assume the layer before it already failed.

Measure Resource Consumption, Not Request Counts

A payment initiation request and a health check request are not the same event, and this platform's monitoring treated them as if they were. A health check costs milliseconds. A payment request can trigger fraud scoring, AML screening, balance verification, ledger updates, audit logging, and a notification pipeline.

Counting requests alone hides that gap entirely. A thousand health checks can cost less than ten payment requests. The attacker in this scenario understood that math from the start. The monitoring on the platform didn't, until it was too late to matter.

Architecture Is Part of Security

Nothing about this attack required SQL injection, remote code execution, broken encryption, or an authentication bypass. Every technical control on the platform did exactly what it was built to do.

Distributed gateways worked as designed. Uneven throttling was a known tradeoff, not an oversight. The business logic performed its fraud checks correctly. The worker pools had reasonable, finite limits. Every piece behaved. The failure showed up only in how those pieces fit together, and that's precisely what makes this kind of application-layer attack so hard to defend against after the fact.

Final Thoughts

Most security conversations focus on keeping attackers out. Availability forces a different question onto the table: what happens once they're already inside the front door with a valid token in hand?

If an authenticated request can force a platform to burn a disproportionate amount of its own resources, that's an attack surface no CDN or firewall rule is going to close. This mobile money platform didn't need to reject every malicious request to survive. It needed to be built assuming some of them would always get through, and stay standing anyway.

Resilience like that isn't something you add after the system is live. It's a decision made in the architecture, long before the first request ever reaches production.

Comments

Popular Posts

Exploiting MS17-010 EternalBlue: SMB Flaw to SYSTEM Access

God Never Wrote a Book: A Nigerian Agnostic's Case

How I Patched CVE-2026-42945 on Monesize Nginx