system-design

DDoS Protection and Abuse Prevention

When the attack isn’t stealing data but drowning you in traffic. You can’t out-provision a botnet — so the game is filtering, absorbing, and making abuse expensive.

Prerequisites: Rate Limiting, CDN Time to read: ~18 minutes


The problem

A Denial of Service attack tries to make your system unavailable — not by breaking in, but by exhausting a resource: bandwidth, connections, CPU, memory, or a downstream dependency.

A Distributed Denial of Service (DDoS) does it from thousands or millions of sources (a botnet), so you can’t just block one IP.

🚨 The asymmetry is the whole problem: attacks are cheap, defence is expensive. An attacker rents a botnet for a few hundred dollars and generates terabits of traffic. You cannot buy enough servers to absorb it — and even if you could, you’d be paying to serve attack traffic. The defence is not “more capacity” but filtering the attack out before it reaches you.


The three layers of attack

DDoS attacks target different layers, and they need different defences.

Volumetric (L3/L4) — flood the pipe

Overwhelm your bandwidth with sheer volume — UDP floods, ICMP floods, and especially amplification attacks.

🚨 Amplification is how small attacks become huge: the attacker sends a small request to a misconfigured third-party server (DNS, NTP, memcached) with your IP spoofed as the source. The server sends a large response to you. A 60-byte request can trigger a 4,000-byte response — a 70× (or with memcached, 50,000×) amplification. The attacker’s bandwidth is multiplied against you.

Defence: you cannot absorb this yourself. You need a network with more capacity than the attack — a scrubbing service or CDN that soaks up terabits and forwards only clean traffic.

Protocol (L3/L4) — exhaust connection state

Exhaust server resources with malformed or half-open connections. The classic is a SYN flood: send TCP SYN packets, never complete the handshake, and fill the server’s connection table with half-open connections until it can’t accept real ones. → TCP

Defence: SYN cookies (the server doesn’t allocate state until the handshake completes), connection rate limiting, and stateful firewalls — mostly handled by your infrastructure or CDN.

Application (L7) — expensive requests

🚨 The hardest to defend and increasingly common. Instead of raw volume, send legitimate-looking requests that are individually expensive — a costly search query, a report generation, a login (which triggers password hashing), or a GraphQL query with deep nesting. A few thousand of these can exhaust your CPU or database while looking like normal traffic.

Why it’s hard: the requests are valid HTTP, from real-looking clients, so simple volume-based filtering doesn’t catch them. You have to distinguish abuse from legitimate use at the application level.

Defence: rate limiting, CAPTCHA, behavioural analysis, query cost limits, and caching so repeated expensive requests are cheap.


The layered defence

No single control stops DDoS. You layer them, filtering as early and cheaply as possible.

flowchart TB
    A[Attack traffic] --> CDN[CDN / Anycast edge<br/>absorb volumetric, spread load]
    CDN --> S[Scrubbing / WAF<br/>filter malicious patterns]
    S --> RL[Rate limiting<br/>per-IP, per-user, global]
    RL --> LS[Load shedding<br/>drop low-priority under pressure]
    LS --> APP[Your application]

🚨 The principle: reject as early and as cheaply as possible. A request blocked at the CDN edge costs you nothing; one that reaches your database has already consumed CPU, a connection, and a query. Each layer removes more, so less reaches the expensive layers.

1. Anycast + CDN (the foundation). A CDN with anycast spreads attack traffic across hundreds of global PoPs, so no single location is overwhelmed, and its enormous aggregate capacity absorbs volumetric floods. 🚨 This is the single most important DDoS defence — the CDN’s capacity dwarfs any botnet, and it’s distributed. Cloudflare, Akamai, AWS Shield, and Google Cloud Armor all provide this.

2. WAF (Web Application Firewall). Filters malicious request patterns — known attack signatures, SQL injection attempts, bad bots. Sits at the edge.

3. Rate limiting. Per-IP, per-user, per-endpoint, and global. → Rate Limiting

4. Load shedding. Under overload, drop low-priority traffic to keep serving the essential. → Resilience Patterns

🚨 Origin protection is critical: if attackers find your origin’s real IP, they bypass all of the above. Lock the origin firewall to accept traffic only from the CDN’s IP ranges, and don’t leak the origin IP (via DNS history, an unproxied subdomain like mail., or certificate transparency logs). → CDN


Abuse prevention (the broader problem)

DDoS is the loud version. There’s a quieter category of abuse that also needs designing against — attackers using your system as intended but at scale or for the wrong purpose:

Credential stuffing / brute force — trying stolen username/password pairs at scale. Defences: rate limiting per account and per IP, MFA, breached-password checks, CAPTCHA after failures, and 🚨 not revealing whether a username exists.

Scraping — bulk-extracting your data. Defences: rate limiting, bot detection, CAPTCHA, cursor-pagination limits, and legal terms.

Spam / content abuse — fake accounts, spam posts. Defences: email/phone verification, rate limits on creation, content filtering, reputation systems.

Resource abuse / denial of wallet — 🚨 an increasingly important one in the cloud/serverless era: triggering your costs. Attackers hammer a serverless function or a metered API to run up your bill (you scale up and pay, rather than falling over). Defences: hard concurrency/spend limits, budget alarms, and rate limiting. → Serverless

Fraud — abusing business logic (fake orders, coupon abuse, fake reviews). A whole domain of its own. → Fraud Detection


The economics: make abuse expensive

🚨 The strategic frame worth internalizing: you can rarely make attacks impossible, so make them uneconomical. Every defence raises the attacker’s cost or lowers their return:

The goal is to shift the attacker toward an easier target. Perfect security is impossible; making yourself more expensive to attack than the next target is achievable.


Detection and response

🚨 You can’t defend what you don’t see. DDoS response depends on detection:


⚖️ Trade-offs

Defence Protects against Cost
CDN / anycast Volumetric floods Money; another layer; origin must be hidden
WAF Known attack patterns False positives blocking real users
Rate limiting Brute force, scraping, L7 floods Legitimate heavy users hit limits
CAPTCHA Bots, automation Real-user friction; accessibility issues
Load shedding Overload Some legitimate requests dropped
Cost limits Denial of wallet May cap legitimate spikes

🚨 Every defence has a false-positive cost — blocking real users. Aggressive DDoS protection during an attack (challenge every request) protects availability but degrades the experience for everyone, including legitimate users. It’s a deliberate trade you make during an attack, not by default.


In the real world


🚨 Interview traps


🎙️ Soundbites


🛠️ Try it

1. Load-test your own service to failure. Use k6 or wrk to hammer an endpoint and find where it falls over — and how. Does it run out of connections, CPU, memory, or database connections? That’s your DDoS weak point, and knowing it is the first step to defending it.

2. Find an expensive endpoint. In your own app, identify the request that costs the most (a search, a report, a login with hashing). Calculate how many per second would exhaust your resources. That’s your L7 attack surface — usually far fewer requests than you’d expect.

3. Simulate rate limiting under attack. Add per-IP rate limiting, then attack from a few “IPs” (spoofed in a header) and confirm they get throttled while a legitimate client still works. Then attack from many IPs and watch per-IP limiting fail — demonstrating why you need global limits and CDN-level defence too.

4. Check your origin exposure. For any site behind a CDN, try to find the real origin IP via DNS history tools or certificate transparency logs (crt.sh). If you can find it, so can an attacker — and they’d bypass the CDN.


Check yourself

1. Why can't you defend against a volumetric DDoS by adding capacity? Because the economics are hopelessly asymmetric. An attacker can rent a botnet or use amplification to generate terabits per second of traffic for a few hundred dollars, while provisioning enough infrastructure to absorb that would cost you enormously — and you'd be paying to *serve attack traffic* that produces no value. Even the largest single origins can't match the tens of terabits per second of the biggest attacks. The only viable defence is *filtering the attack out before it reaches you*, using a network whose aggregate capacity genuinely exceeds any attack — a CDN or scrubbing service with hundreds of Tbps spread across hundreds of anycast locations, which absorbs the flood and forwards only clean traffic. You defend by filtering, not by out-provisioning.
2. What is an amplification attack and why is it dangerous? The attacker sends a small request to a misconfigured public server (DNS, NTP, memcached) but spoofs the *source* IP to be the victim's. The server sends its (much larger) response to the victim, who never asked. Because the response is many times bigger than the request — 70× for DNS, up to 50,000× for memcached — the attacker multiplies their own bandwidth against the target: a modest attacker connection generates a massive flood at the victim. It's dangerous because it lets a small attacker produce enormous traffic, it uses innocent third-party servers as unwitting weapons (making it hard to trace and block at source), and the spoofed source means the victim can't easily identify the real attacker. It's a major component of the largest recorded DDoS attacks.
3. Why are application-layer (L7) DDoS attacks harder to defend than volumetric ones? Because the traffic looks legitimate. A volumetric attack is obviously abnormal — a flood of malformed packets or raw volume that a CDN can identify and drop by pattern. An L7 attack sends *valid* HTTP requests from real-looking clients, but chooses requests that are individually expensive: a costly search, a report generation, a login that triggers password hashing, a deeply-nested GraphQL query. A few thousand of these exhaust your CPU or database while being indistinguishable, at the network level, from genuine users doing normal things. So you can't filter them by volume or signature; you have to distinguish abuse from legitimate use at the application level — via rate limiting by cost, behavioural analysis, CAPTCHA, query-cost limits, and caching so repeated expensive requests become cheap. It's the boundary between "block obvious garbage" and "is this a real user?", which is genuinely hard.
4. Why must the origin be hidden when using a CDN for DDoS protection, and how does it leak? Because the CDN can only protect traffic that flows through it. If an attacker discovers your origin server's real IP address, they send the attack directly to the origin, completely bypassing the CDN's filtering and absorption — so all your protection is worthless. The origin leaks through several channels: DNS history services (which record the IP before you moved behind the CDN), unproxied subdomains that still point directly at origin (`mail.example.com`, `ftp.`, `staging.`), certificate transparency logs (which list every certificate issued for your domains), email headers (originating IP), and misconfigured services that reveal it. Defences: firewall the origin to accept traffic *only* from the CDN's published IP ranges (or use an authenticated private link), rotate the origin IP after enabling the CDN, proxy *every* DNS record, and audit for leaks.
5. What is "denial of wallet" and why is it a concern for serverless architectures? An attack (or bug) that inflicts cost rather than downtime by triggering your usage-based charges. In a traditional fixed-capacity system, excess traffic causes degradation or an outage — bad, but bounded. In a serverless or auto-scaling metered system, the platform *scales up to meet the load and bills you for it*: an attacker hammering a Lambda function or a metered API doesn't take you down, they run up your bill, potentially to five or six figures, because per-request billing has no natural ceiling. A recursive trigger (a function writing to the bucket that invokes it) or a retry loop can do it accidentally. Defences: hard concurrency limits per function, account-level spend/budget alarms with automated response, rate limiting on public endpoints, and extreme care with any trigger whose output could re-invoke it. It's a reason to set these limits from day one rather than after the first surprise bill.

Further reading