The Anatomy of a DDoS Attack — and How to Defend Against It
DDoS — Distributed Denial of Service — sounds dramatic because it is. It’s not a single hacker hammering your endpoint; it’s many machines (often thousands) all asking for something until your service can’t keep up. The result is the same: users can’t reach your site, APIs time out, and dashboards scream.
The good news: most DDoS scenarios are predictable and containable if you design your system with defense-in-depth. I’ll walk you through the attack types, practical defenses (network → infra → app), Java/Spring examples you can use right away, and an incident checklist to follow when the alarms go off.
Quick taxonomy: common DDoS types (and why they hurt)
- Volumetric attacks — raw bandwidth floods (UDP floods, DNS amplification). Goal: saturate network links.
- Protocol attacks — exploit protocol-state weaknesses (SYN flood, TCP connection exhaustion). Goal: exhaust server resources like connection tables.
- Application-layer attacks — well-formed HTTP requests hitting expensive endpoints (e.g.,
/search?q=*), often indistinguishable from legitimate traffic. Goal: cause CPU/DB load or force expensive computations. - Slow attacks — very slow requests that keep connections open (Slowloris). Goal: consume concurrency slots.
Each type needs different defenses. Let’s cover them from the edge in.
Network & edge defenses (first line of defense)
1) Use a CDN + Anycast + Scrubbing
A CDN (Cloudflare, Fastly, Akamai) buys you:
- Massive bandwidth and distribution (mitigates volumetric attacks).
- Anycast routing so traffic spreads across POPs.
- Traffic scrubbing / challenge pages (CAPTCHA, JS challenges) to filter bots.
Rule: Put the CDN in front of your origin. Don’t expose origin IPs publicly.
2) DDoS protection at the provider
If you run on cloud (AWS/GCP/Azure), enable their DDoS protections (AWS Shield, Google Cloud Armor, Azure DDoS). These services monitor traffic patterns and automatically mitigate large attacks.
3) Network-level controls (firewall / ACL / rate-limits)
At the edge you should be able to:
- Blackhole or null-route clearly malicious prefixes (if needed).
- Apply rate limits per-source and per-prefix (but be careful with NATed clients).
- Drop UDP floods and large ICMP using network ACLs.
Simple iptables example to drop traffic rate-limited by IP (quick blunt tool — use with care):
# limit new connections to 20 per minute per IP
iptables -A INPUT -p tcp --dport 80 -m connlimit --connlimit-above 20 -j DROP
# or use recent module for burst control
iptables -A INPUT -p tcp --dport 80 -m recent --update --seconds 60 --hitcount 100 -j DROP
(Use provider-managed firewalls when possible — they are safer than manual iptables on public servers.)
Infrastructure & load-level defenses
4) Autoscaling and graceful degradation
Autoscaling helps with moderate spikes but is not a silver bullet for volumetric floods (those saturate the network). Still, for application-layer spikes, autoscaling can buy you time.
Design graceful degradation:
- Return simple cached pages for unauthenticated users.
- Turn off non-critical features under heavy load (analytics, recommendations).
- Prefer circuit breakers to fail fast when downstream dependencies are overloaded.
Example using Resilience4j (Spring) to add a circuit breaker for a downstream service:
// build a circuit breaker that opens after 5 failures
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.failureRateThreshold(50)
.slidingWindowSize(10)
.waitDurationInOpenState(Duration.ofSeconds(30))
.build();
CircuitBreakerRegistry registry = CircuitBreakerRegistry.of(config);
CircuitBreaker cb = registry.circuitBreaker("bankAdapter");
Supplier<String> protectedCall = CircuitBreaker.decorateSupplier(cb, () -> bankClient.call());
Try<String> result = Try.ofSupplier(protectedCall)
.recover(throwable -> "fallback");
5) Connection limits and TCP tuning (server side)
Set sensible connection and request timeouts in your app server (Tomcat, Jetty) and reverse proxy (nginx). Reject slow requests early.
Example (application.yml for Spring Boot with embedded Tomcat):
server:
tomcat:
max-threads: 200
accept-count: 100
connection-timeout: 10000 # milliseconds
Also tune kernel TCP settings to mitigate SYN floods (net.ipv4.tcp_syncookies = 1, etc.). Many hosting providers offer SYN-filtering; use it.
Application-layer defenses (where you can be surgical)
6) Rate limiting at API gateway / edge + token buckets
Rate limiting is your friend. Do it in the API gateway or at the CDN edge. Common patterns:
- Global rate limits per IP.
- User-specific rate limits (for authenticated users).
- Leaky bucket / token bucket algorithms.
Java example using Bucket4j (in a Spring filter):
@Component
public class RateLimitFilter extends OncePerRequestFilter {
private final Bucket bucket = Bucket4j.builder()
.addLimit(Bandwidth.classic(100, Refill.greedy(100, Duration.ofMinutes(1))))
.build();
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (bucket.tryConsume(1)) {
filterChain.doFilter(request, response);
} else {
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
response.getWriter().write("Too many requests");
}
}
}
This example is global for simplicity — in production you’d key buckets by IP, user, or API key and persist to Redis to share limits across instances.
7) Auth + API keys + per-client quotas
Require authentication for actions that are expensive (like bulk exports). Apply per-client quotas and logging to detect unusual behavior.
8) Request validation & cheap rejection
Reject invalid or obviously abusive requests early:
- Tight schema validation (JSON schema).
- Limit request body size.
- Reject requests missing essential headers.
This reduces wasted CPU on parsing nonsense.
Example: Spring @ControllerAdvice to reject large uploads:
@Bean
public FilterRegistrationBean<HttpPutFormContentFilter> multipartFilter() {
MultipartConfigElement multipartConfigElement = new MultipartConfigElement("/tmp", 5_242_880, 20_971_520, 0);
MultipartConfigFactory factory = new MultipartConfigFactory();
factory.setMaxFileSize(DataSize.ofMegabytes(5));
FilterRegistrationBean<HttpPutFormContentFilter> registration = new FilterRegistrationBean<>(new HttpPutFormContentFilter());
return registration;
}
9) Caching & CDN offload
Cache static and cheap-to-produce responses aggressively. If an attack targets pages that can be cached, the CDN answers them and your origin is safe.
Set proper cache headers:
@GetMapping("/docs")
public ResponseEntity<String> getDocs() {
return ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(24, TimeUnit.HOURS))
.body(generateDocs());
}
Bot & behavior detection (smart filtering)
10) Behavioral anomaly detection
Application-layer DDoS often imitates normal traffic. Use behavioral signals:
- Request patterns (burstiness, same path),
- Client fingerprints (UA, headers, JS challenges),
- Session cookie patterns.
CDNs have built-in bot management. You can also implement server-side heuristics: slow request rate from normal browsers vs super-fast repeat calls from script clients.
11) Challenge-response
For suspicious clients, present a challenge (JavaScript challenge, CAPTCHA). Legitimate browsers solve it; dumb bots often fail.
Observability & alerting — you must see the attack early
If you can’t detect it quickly, you can’t mitigate it quickly.
12) Instrument everything
- Metrics: request rate, 5xx rate, p95 latency, connection counts. Use Micrometer + Prometheus + Grafana in Java apps.
- Tracing: track requests across services to find hotspots.
- Logs: structured logs with request IDs and client IPs.
Example: Micrometer counter for suspicious requests in Spring:
@Autowired
MeterRegistry meterRegistry;
public void onSuspiciousRequest(String ip) {
Counter.builder("suspicious.requests")
.tag("ip", ip)
.register(meterRegistry)
.increment();
}
13) Alerting
Alert on sudden spikes in:
- total request rate,
- CPU,
- SYN/connection queues,
- error rates.
Have runbooks ready (see below).
Incident response: what to do when alarm rings
- Triage fast
- Identify type: volumetric vs app-layer vs protocol. Look at traffic graphs and packet details.
- Activate DDoS protections
- Enable CDN challenge or provider DDoS protection.
- Block obvious malicious sources (temporarily)
- Null-route, block prefixes, or apply firewall rules — be careful with collateral damage.
- Rate-limit / throttle
- Apply stricter limits at the gateway and edge.
- Scale safe paths
- Spin up more instances for API endpoints that are legitimate and cacheable; disable heavy endpoints temporarily.
- Switch to read-only / maintenance mode
- If necessary, prevent writes to avoid data corruption.
- Forensic
- Capture pcap, logs, and save metrics for postmortem.
- Communicate
- Post status updates to users — silence is worse.
- Post-incident
- Analyze logs, make rules permanent if needed, and refine detection logic.
Testing & practice (don’t wait until it happens)
- Use load testing tools (k6, locust) to simulate traffic spikes and tune limits.
- Test your failover and scrubbing procedures in staging.
- Perform chaos engineering experiments (with caution) to ensure graceful degradation works.
Example k6 scenario:
import http from 'k6/http';
import { sleep } from 'k6';
export default function () {
http.get('https://yourapi.example.com/resource');
sleep(1);
}
Run for high concurrency to see how your infrastructure responds.
Cost, trade-offs, and realistic expectations
- CDN + DDoS service = peace of mind, costs money. For production-facing services, it’s worth it.
- Autoscaling helps but not for network-saturating attacks.
- Rate limiting / bot management needs tuning to avoid blocking real users — start permissive, tighten as you measure.
- Logging & monitoring cost storage but save hours during incidents.
Practical checklist (copy-paste into your on-call handbook)
- CDN in front of origin; origin IP hidden
- Provider DDoS protection enabled (Shield/Armor/etc.)
- Rate limiting at edge and per-client quotas
- Circuit breakers around downstream dependencies (Resilience4j)
- Connection and request timeouts configured (Tomcat/nginx)
- Kernel TCP SYN cookies enabled on servers
- Structured logs and metrics (Micrometer/Prometheus)
- Alerting for spikes (request rate, SYN, 5xx)
- Playbook for null-routing and scrubbing activation
- Regular load testing and incident drills
Short Spring/Java snippets recap
Rate limiting (Bucket4j) — add a token bucket per IP or API key in a Redis-backed store for shared state.
Circuit breaker (Resilience4j) — prevent retry storms and backpressure cascades.
Micrometer — instrument failures/latencies and push to Prometheus/Grafana for quick observability.
Tomcat timeouts / thread limits — avoid thread exhaustion by capping max threads and forcing short timeouts.
Final thoughts — design like you might be attacked
DDoS is noisy and unpleasant, but a lot of damage is avoidable. Architect for the predictable: distribute, cache, reject early, monitor, and automate your defenses. If you put one thing in place today, make it a CDN with bot management and rate-limits at the edge — that alone solves a huge fraction of real-world attacks.
If you want, I can convert this into a concise on-call playbook PDF or generate sample nginx/CDN rules you can drop into your environment. Want that?
If this helped, consider sharing it with a colleague who’s on-call this week — they’ll thank you later. And if you want bite-sized versions of these lessons, follow me on X where I share practical dev learnings every day.
Member discussion