DDoS #
Distributed Denial of Service (DDoS) is an attack aimed at making a service unavailable to legitimate users — not by exploiting logic weaknesses or stealing data, but in a more direct way: flooding the system with traffic until resources are exhausted and legitimate requests can’t be served.
What makes DDoS hard to deal with is its asymmetric nature. Attackers send millions of requests at very low cost — using botnets, amplification techniques, or cheap cloud instances — while victims must provide enough resources to serve every request, including fake ones. Building enough capacity to withstand large-scale attacks is often uneconomical. A more realistic approach combines early detection, filtering, rate limiting, and relying on infrastructure designed for scale.
But not all “DDoS” comes from attackers. Sudden traffic spikes from viral content, products that suddenly become popular, or uncontrolled bots can have the same effect: systems unavailable. Good DDoS mitigation protects against all these scenarios at once.
The Three DDoS Categories #
Understanding attack categories helps determine the right mitigation — different categories, different approaches.
graph TD
A[DDoS Attack] --> B[Volumetric]
A --> C[Protocol]
A --> D["Application Layer\nLayer 7"]
B --> B1["UDP Flood\nICMP Flood\nDNS Amplification\nNTP Amplification"]
C --> C1["SYN Flood\nPing of Death\nSmurf Attack"]
D --> D1["HTTP Flood\nSlowloris\nRudy Attack\nAPI Abuse"]
B1 --> E["Bandwidth Saturation\nGbps - Tbps"]
C1 --> F["Connection Table Saturation\nOS/Network Level"]
D1 --> G["Exhausts Server Resources\nCPU/Memory/DB Connection"]Volumetric Attacks #
Bandwidth saturation — sending traffic in volumes exceeding the victim’s network capacity. This is what often appears in the news: “1 Tbps DDoS attack.”
DNS Amplification Attack — a volumetric example:
Technique: attackers send DNS queries with spoofed source IPs
(source IP = the victim's IP)
1. Attackers send queries to thousands of public DNS resolvers:
Source IP: victim.com (spoofed)
Query: ANY example.com (a query producing a large response)
2. DNS resolvers respond to the victim (not the attacker):
Query: ~40 bytes
Response: ~3000 bytes → 75x amplification factor!
3. Thousands of DNS resolvers simultaneously flood the victim
with traffic they never asked for
4. The victim's bandwidth saturates
1000 resolvers × 75x amplification = 75,000x attacker traffic
Mitigations:
→ ISP/CDN: anycast + scrubbing centers
→ DNS: rate limit responses, Response Rate Limiting (RRL)
→ Can't be handled at the application level — needs infrastructure
Protocol Attacks #
Exploiting weaknesses in network protocols to exhaust resources at the network device or OS level.
SYN Flood:
Normal TCP 3-way handshake:
Client → Server: SYN
Server → Client: SYN-ACK
Client → Server: ACK ← connection established
SYN Flood attack:
Attacker → Server: SYN (with a spoofed source IP)
Server → ? : SYN-ACK (can't arrive — fake IP)
Server waits for ACK... (30-120 seconds)
Attackers send millions of SYNs → the server stores millions of half-open connections
Connection table full → no new connections accepted
The server can't serve legitimate users
Mitigations:
→ SYN cookies: the server stores no state until an ACK arrives
→ Firewalls/Load balancers: limit half-open connections per IP
→ OS tuning: net.ipv4.tcp_syncookies = 1
Application Layer Attacks (Layer 7) #
Attacks simulating legitimate requests — hard to distinguish from real traffic, no large bandwidth needed.
// Slowloris example — one machine can make a server unresponsive
// Attackers send HTTP headers slowly, never completing the request
// Conceptual simulation (don't use for attacks):
// import "net"
// sockets := make([]net.Conn, 0, 200)
// for i := 0; i < 200; i++ {
// s, _ := net.Dial("tcp", target+":80")
// s.Write([]byte("GET / HTTP/1.1\r\n"))
// s.Write([]byte("Host: target.com\r\n"))
// sockets = append(sockets, s)
// }
//
// for {
// for _, s := range sockets {
// s.Write([]byte("X-a: b\r\n")) // send headers one by one, very slowly
// }
// time.Sleep(15 * time.Second)
// }
// The server keeps these connections open because the headers aren't complete
// 200 connections can take down an Apache server with a small MaxClients
// Slowloris mitigations:
// → Nginx is more resilient than Apache (event-driven, not thread-per-connection)
// → Strict request timeouts
// → Per-IP connection limits
Rate Limiting: The First Defense Layer in Applications #
Rate limiting restricts how many requests one client can make within a period. It doesn’t stop large-scale DDoS, but it’s very effective at limiting impact and protecting against API abuse and scraping.
// Layered rate limiting implementation with Redis
// redisClient is a go-redis client (import "github.com/redis/go-redis/v9")
var redisClient = redis.NewClient(&redis.Options{Addr: "redis:6379"})
var ctx = context.Background()
// RateLimiter uses the sliding window algorithm.
// More accurate than fixed windows — no spikes at window boundaries.
type RateLimiter struct {
Redis *redis.Client
MaxRequests int
WindowSeconds int
}
type RateLimitResult struct {
Allowed bool
Remaining int
ResetAt int64
}
func (rl *RateLimiter) IsAllowed(identifier string) RateLimitResult {
now := time.Now().Unix()
key := "rate:" + identifier
// Remove entries outside the window, count, add, set expiry — one pipeline
pipe := rl.Redis.TxPipeline()
pipe.ZRemRangeByScore(ctx, key, "0", strconv.FormatInt(now-int64(rl.WindowSeconds), 10))
card := pipe.ZCard(ctx, key)
pipe.ZAdd(ctx, key, redis.Z{Score: float64(now), Member: now})
pipe.Expire(ctx, key, time.Duration(rl.WindowSeconds)*time.Second)
_, _ = pipe.Exec(ctx)
currentCount := int(card.Val())
return RateLimitResult{
Allowed: currentCount < rl.MaxRequests,
Remaining: max(0, rl.MaxRequests-currentCount-1),
ResetAt: now + int64(rl.WindowSeconds),
}
}
// Several limiters with different thresholds for different endpoints
// Global: all endpoints
var globalLimiter = &RateLimiter{Redis: redisClient, MaxRequests: 1000, WindowSeconds: 60}
// Sensitive API endpoints: stricter
var apiLimiter = &RateLimiter{Redis: redisClient, MaxRequests: 100, WindowSeconds: 60}
// Auth endpoints: very strict to prevent brute force
var authLimiter = &RateLimiter{Redis: redisClient, MaxRequests: 10, WindowSeconds: 300}
// Wrapper replacing the Python decorator.
// identifierFunc resolves the client identifier (IP, user, or a combination).
func rateLimit(limiter *RateLimiter, identifierFunc func() string, handler http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Identifier: per IP, per user, or a combination
identifier := r.RemoteAddr
if identifierFunc != nil {
identifier = identifierFunc()
}
result := limiter.IsAllowed(identifier)
if !result.Allowed {
writeJSON(w, http.StatusTooManyRequests, map[string]string{
"error": "Rate limit exceeded",
"message": "Too many requests. Please try again later.",
})
return
}
// Add rate limit info to the response header
w.Header().Set("X-RateLimit-Limit", strconv.Itoa(limiter.MaxRequests))
w.Header().Set("X-RateLimit-Remaining", strconv.Itoa(result.Remaining))
w.Header().Set("X-RateLimit-Reset", strconv.FormatInt(result.ResetAt, 10))
handler(w, r)
}
}
// Usage:
// http.HandleFunc("/api/data", rateLimit(apiLimiter, nil, getData))
// http.HandleFunc("/login", rateLimit(authLimiter, nil, login))
// Per-user rate limiting (authenticated):
func getUserIdentifier() string {
if currentUserIsAuthenticated() {
return "user:" + currentUserID()
}
return "ip:" + requestRemoteAddr()
}
// http.HandleFunc("/api/expensive-operation", rateLimit(apiLimiter, getUserIdentifier, expensiveOperation))
Circuit Breakers to Protect Dependencies #
When one service is flooded with requests, the effect can cascade to other services depending on it. Circuit breakers cut off the request flow to a struggling service, giving it time to recover.
// Circuit breaker protecting external calls from cascade failures.
type CircuitState int
const (
StateClosed CircuitState = iota // Normal — requests allowed
StateOpen // Failing — requests rejected immediately
StateHalfOpen // Testing recovery — some requests allowed
)
type CircuitBreaker struct {
failureThreshold int // fail N times → OPEN
recoveryTimeout time.Duration // wait N seconds before HALF_OPEN
successThreshold int // succeed N times in HALF_OPEN → CLOSED
mu sync.Mutex
state CircuitState
failureCount int
successCount int
lastFailureAt time.Time
}
func NewCircuitBreaker(failureThreshold, recoveryTimeoutSeconds, successThreshold int) *CircuitBreaker {
return &CircuitBreaker{
failureThreshold: failureThreshold,
recoveryTimeout: time.Duration(recoveryTimeoutSeconds) * time.Second,
successThreshold: successThreshold,
state: StateClosed,
}
}
// ErrCircuitOpen is returned while the breaker is open.
var ErrCircuitOpen = errors.New("circuit breaker open — service unavailable")
func (cb *CircuitBreaker) Call(fn func() (interface{}, error)) (interface{}, error) {
cb.mu.Lock()
if cb.state == StateOpen {
// Check whether it's time for recovery
if time.Since(cb.lastFailureAt) > cb.recoveryTimeout {
cb.state = StateHalfOpen
cb.successCount = 0
} else {
cb.mu.Unlock()
return nil, ErrCircuitOpen
}
}
cb.mu.Unlock()
result, err := fn()
if err == nil {
cb.mu.Lock()
if cb.state == StateHalfOpen {
cb.successCount++
if cb.successCount >= cb.successThreshold {
cb.state = StateClosed
cb.failureCount = 0
}
}
cb.mu.Unlock()
return result, nil
}
cb.mu.Lock()
cb.failureCount++
cb.lastFailureAt = time.Now()
if cb.failureCount >= cb.failureThreshold {
cb.state = StateOpen
log.Printf("Circuit breaker OPEN: too many failures (count=%d)", cb.failureCount)
}
cb.mu.Unlock()
return nil, err
}
// Usage:
var (
dbCircuit = NewCircuitBreaker(5, 30, 2)
paymentCircuit = NewCircuitBreaker(3, 60, 2)
)
// user, err := dbCircuit.Call(func() (interface{}, error) { return userRepo.FindByID(userID) })
// payment, err := paymentCircuit.Call(func() (interface{}, error) { return paymentGateway.Charge(amount, card) })
Caching as a DDoS Shield #
Aggressive caching drastically reduces backend load — even during a DDoS, many requests can be served from cache without touching the database.
// Aggressively cache endpoint responses in Redis to reduce backend load.
// redisClient is a go-redis client (import "github.com/redis/go-redis/v9")
var redisClient = redis.NewClient(&redis.Options{Addr: "redis:6379"})
var ctx = context.Background()
// CacheResponse caches endpoint responses.
// varyOn lists the query parameters that affect the cache key.
func CacheResponse(ttl time.Duration, varyOn ...string) func(http.HandlerFunc) http.HandlerFunc {
return func(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Build the cache key from the endpoint + parameters
keyParts := []string{r.URL.Path}
if len(varyOn) > 0 {
for _, param := range varyOn {
value := r.URL.Query().Get(param)
keyParts = append(keyParts, param+"="+value)
}
} else {
// Default: vary on all query params
query := r.URL.Query()
sortedParams := make([]string, 0, len(query))
for k, v := range query {
sortedParams = append(sortedParams, k+"="+v[0])
}
sort.Strings(sortedParams)
keyParts = append(keyParts, sortedParams...)
}
sum := md5.Sum([]byte(strings.Join(keyParts, "|")))
cacheKey := "cache:" + hex.EncodeToString(sum[:])
// Check the cache
cached, err := redisClient.Get(ctx, cacheKey).Result()
if err == nil {
w.Header().Set("X-Cache", "HIT")
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(cached))
return
}
// Cache miss — run the handler through a recorder
rec := httptest.NewRecorder()
next(rec, r)
for k, v := range rec.Header() {
w.Header()[k] = v
}
w.Header().Set("X-Cache", "MISS")
w.WriteHeader(rec.Code)
// Store in the cache when the response is OK
if rec.Code == http.StatusOK {
_ = redisClient.Set(ctx, cacheKey, rec.Body.String(), ttl).Err()
}
_, _ = w.Write(rec.Body.Bytes())
}
}
}
// Frequently accessed public endpoints — aggressive caching
// http.HandleFunc("/products", CacheResponse(5*time.Minute, "category", "page")(listProducts)) // cache for 5 minutes
// Static endpoints — very long caching
// http.HandleFunc("/api/config", CacheResponse(time.Hour)(getConfig)) // cache for 1 hour
Caching strategies for DDoS resistance:
Level 1 — CDN cache (most effective against volumetric attacks):
→ Static assets (JS, CSS, images) served from CDN edges
→ Attackers flood the CDN, not the origin server
→ CDNs have far larger capacity than regular servers
Level 2 — Application cache (Redis/Memcached):
→ Unchanging API responses cached aggressively
→ Significantly reduces database load
→ Even under attack, cached responses can be served
Level 3 — Database query cache:
→ Identical queries not executed repeatedly
→ Efficient connection pools
Trade-off: data can be stale
→ Determine TTLs by how fresh data needs to be
→ Use cache invalidation for data that must always be fresh
Traffic Anomaly Detection #
Not every traffic spike is a DDoS. But unexpected spikes disproportionate to normal patterns are indications needing investigation.
// Monitor traffic and detect anomalies.
type TrafficMonitor struct {
windowSize int
thresholdMultiplier float64
requestCounts []requestCount // (timestamp, count)
baselineRPS float64 // requests per second baseline
}
type requestCount struct {
at time.Time
count int
}
func NewTrafficMonitor(windowSize int, thresholdMultiplier float64) *TrafficMonitor {
return &TrafficMonitor{windowSize: windowSize, thresholdMultiplier: thresholdMultiplier}
}
func (m *TrafficMonitor) RecordRequests(count int) {
now := time.Now()
m.requestCounts = append(m.requestCounts, requestCount{at: now, count: count})
// Remove data outside the window
cutoff := now.Add(-time.Duration(m.windowSize) * time.Second)
i := 0
for i < len(m.requestCounts) && m.requestCounts[i].at.Before(cutoff) {
i++
}
m.requestCounts = m.requestCounts[i:]
}
func (m *TrafficMonitor) CurrentRPS() float64 {
if len(m.requestCounts) == 0 {
return 0
}
total := 0
for _, rc := range m.requestCounts {
total += rc.count
}
return float64(total) / float64(m.windowSize)
}
func (m *TrafficMonitor) IsAnomalous() bool {
current := m.CurrentRPS()
if m.baselineRPS == 0 {
return false
}
// Anomaly if traffic exceeds N times the baseline
return current > m.baselineRPS*m.thresholdMultiplier
}
func (m *TrafficMonitor) UpdateBaseline(rps float64) {
if m.baselineRPS == 0 {
m.baselineRPS = rps
} else {
// Exponential moving average
m.baselineRPS = 0.9*m.baselineRPS + 0.1*rps
}
}
// Detection based on IP distribution
type IPAnomalyDetector struct {
window int
ipCounts map[string][]time.Time
}
func NewIPAnomalyDetector(windowSeconds int) *IPAnomalyDetector {
return &IPAnomalyDetector{window: windowSeconds, ipCounts: map[string][]time.Time{}}
}
func (d *IPAnomalyDetector) RecordRequest(ip string) {
now := time.Now()
d.ipCounts[ip] = append(d.ipCounts[ip], now)
// Remove old data
cutoff := now.Add(-time.Duration(d.window) * time.Second)
times := d.ipCounts[ip]
i := 0
for i < len(times) && times[i].Before(cutoff) {
i++
}
d.ipCounts[ip] = times[i:]
}
type SuspiciousIP struct {
IP string
Count int
Rate float64
}
// Return IPs with more than the threshold of requests in the window.
func (d *IPAnomalyDetector) SuspiciousIPs(threshold int) []SuspiciousIP {
var suspicious []SuspiciousIP
for ip, times := range d.ipCounts {
if len(times) > threshold {
suspicious = append(suspicious, SuspiciousIP{
IP: ip,
Count: len(times),
Rate: float64(len(times)) / float64(d.window),
})
}
}
sort.Slice(suspicious, func(a, b int) bool {
return suspicious[a].Count > suspicious[b].Count
})
return suspicious
}
// Middleware for monitoring
var (
monitor = NewTrafficMonitor(60, 3.0)
ipDetector = NewIPAnomalyDetector(60)
)
func monitorTraffic(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ip := r.RemoteAddr
monitor.RecordRequests(1)
ipDetector.RecordRequest(ip)
// Check for anomalies every N requests to avoid overhead
if monitor.IsAnomalous() {
log.Printf("Traffic anomaly detected: current=%.2f rps baseline=%.2f rps",
monitor.CurrentRPS(), monitor.baselineRPS)
}
// Block very aggressive IPs (can also be done at the nginx level)
suspicious := ipDetector.SuspiciousIPs(200)
for _, s := range suspicious {
if s.IP == ip {
writeJSON(w, http.StatusTooManyRequests, map[string]string{
"error": "Too many requests from your IP",
})
return
}
}
next(w, r)
}
}
Infrastructure Configuration for DDoS Resistance #
Most effective DDoS mitigations don’t happen at the application level — they happen at the infrastructure level.
# Nginx — DDoS resistance configuration
# Rate limiting at the nginx level
limit_req_zone $binary_remote_addr zone=api:10m rate=100r/m;
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
limit_conn_zone $binary_remote_addr zone=conn_per_ip:10m;
server {
listen 443 ssl;
# Limit connections per IP
limit_conn conn_per_ip 20;
# Strict timeouts — prevents Slowloris
client_body_timeout 10s;
client_header_timeout 10s;
keepalive_timeout 30s;
send_timeout 10s;
# Limit request body sizes
client_max_body_size 10m;
# Return 444 (close the connection without a response) for suspicious user agents
if ($http_user_agent ~* (bot|crawler|spider|scraper|scan)) {
return 444;
}
location /api/ {
limit_req zone=api burst=20 nodelay;
limit_req_status 429;
# Proxy to the application
proxy_pass http://app:8000;
proxy_read_timeout 30s;
proxy_connect_timeout 10s;
}
location /login {
limit_req zone=login burst=3 nodelay;
proxy_pass http://app:8000;
}
# Cache static assets at the CDN/browser
location /static/ {
expires 7d;
add_header Cache-Control "public, immutable";
}
}
# CDN and DDoS protection (Cloudflare as an example)
# Configured via Terraform or the dashboard
# Configuration principles:
# 1. Always-on DDoS protection: traffic always flows through the CDN/scrubbing center
# 2. Challenge suspicious traffic: CAPTCHAs or JS challenges for suspicious IPs
# 3. IP reputation blocking: block known-dangerous IPs
# 4. Rate limiting at the edge: before traffic reaches the origin server
# 5. Geo-blocking: if traffic from certain countries is irrelevant to the business
# CDN rate limit configuration (conceptual):
# - /api/*: 1000 requests/minute per IP
# - /login: 10 requests/minute per IP
# - /register: 5 requests/minute per IP
# - Static assets: unlimited (served from cache)
Application-Level DDoS: Resource Exhaustion #
Attackers don’t always need large volumes. They can target resource-heavy endpoints with few requests.
// Endpoints vulnerable to resource exhaustion:
func searchVulnerable(w http.ResponseWriter, r *http.Request) {
// Unbounded complex queries
query := r.URL.Query().Get("q")
results := productRepo.Search(query) // full table scan on a large table
writeJSON(w, http.StatusOK, results)
}
// If attackers send 50 concurrent requests to this endpoint → database overload
// CORRECT: resource-aware endpoints
func searchSafe(w http.ResponseWriter, r *http.Request) {
query := strings.TrimSpace(r.URL.Query().Get("q"))
// Validate the minimum query length
if len(query) < 3 {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Query must be at least 3 characters"})
return
}
// Limit the query length
if len(query) > 100 {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Query too long"})
return
}
// Use a full-text index, not LIKE %...%
// Limit the results
results := productRepo.SearchFullText(query, 50)
writeJSON(w, http.StatusOK, results)
}
// Expensive operations must be queued, not processed synchronously
func generateReport(w http.ResponseWriter, r *http.Request) {
// Don't process directly — put it in a queue
job := reportQueue.Enqueue(GenerateReportTask{
UserID: currentUserID(r),
Payload: r.Body,
JobTimeout: 5 * time.Minute, // maximum 5 minutes
})
writeJSON(w, http.StatusAccepted, map[string]string{ // 202 Accepted
"job_id": job.ID,
"status": "queued",
"check_url": "/reports/status/" + job.ID,
})
}
// Registration (using net/http):
// http.HandleFunc("/search", rateLimit(apiLimiter, nil, searchSafe))
// http.HandleFunc("/reports/generate", rateLimit(authLimiter, nil, generateReport))
Anti-Patterns to Avoid #
// ✗ Anti-pattern 1: no rate limiting at all
// Every endpoint accessible without limits
// One client can make thousands of requests per second
// ✗ Anti-pattern 2: rate limiting too easily bypassed
// Limits only based on IPs — easily bypassed with many IPs
// or by using proxies/VPNs
// ✓ Solution: combine IP + user ID + fingerprinting
// ✗ Anti-pattern 3: synchronous processing for expensive operations
// func exportCSV(w http.ResponseWriter, r *http.Request) {
// data := orderRepo.FindAll() // OOM risk
// generateCSV(w, data) // timeout risk
// }
// ✓ Solution: queue to background workers, return job IDs
// ✗ Anti-pattern 4: no timeouts for downstream dependencies
// resp, err := http.Get("https://external-api.com/data") // no timeout
// ✓ Solution: always set timeouts
// client := &http.Client{Timeout: 10 * time.Second}
// ✗ Anti-pattern 5: caches abusable for cache poisoning
// func getProfile(w http.ResponseWriter, r *http.Request) {
// // Without ownership validation, attackers can cache
// // other users' responses and serve them to others
// profile := userRepo.FindByID(userID)
// writeJSON(w, http.StatusOK, profile)
// }
// ✓ Solution: cache keys must include the authenticated user ID for private data
// ✗ Anti-pattern 6: no traffic monitoring
// DDoS spikes only discovered after systems go down
// ✓ Solution: real-time alerting for RPS anomalies
DDoS Mitigation Checklist #
RATE LIMITING:
□ Rate limiting active on all public endpoints
□ Different thresholds for sensitive endpoints (auth, payment)
□ Rate limit headers sent in responses (Retry-After, X-RateLimit-*)
□ Rate limiting at multiple levels (nginx, application, CDN)
INFRASTRUCTURE:
□ CDN or DDoS protection services active (Cloudflare, AWS Shield, etc.)
□ Anycast routing spreading traffic across multiple PoPs
□ Auto-scaling configured with clear limits
□ Bandwidth capacity calculated for peaks + buffers
NGINX/LOAD BALANCER:
□ Request timeouts configured (prevents Slowloris)
□ Maximum connections per IP limited
□ Request body sizes limited
□ Rate limiting at the nginx level for critical endpoints
CACHING:
□ Cacheable responses aggressively cached
□ Static assets served from CDNs, not origins
□ Cache TTLs configured per freshness needs
CIRCUIT BREAKERS:
□ Circuit breakers active for all external dependencies
□ Fallback behaviors defined when circuits open
□ Recovery timeouts correctly configured
RESOURCE PROTECTION:
□ Resource-intensive endpoints have strict rate limits
□ Expensive operations queued to background workers
□ Timeouts configured for all downstream calls
□ Database connection pools unexhaustible by one endpoint
MONITORING & ALERTING:
□ Alerts installed for RPS exceeding thresholds
□ Alerts for suddenly rising error rates
□ Alerts for significantly rising P99 latency
□ Real-time dashboards for traffic patterns
□ DDoS runbooks available and practiced
INCIDENT RESPONSE:
□ Procedures for enabling additional protections during attacks
□ ISP/CDN escalation contacts available
□ IP blocking executable quickly (nginx, CDN, firewalls)
□ Communication plans for users during downtime
Summary #
- DDoS has three different categories — volumetric (bandwidth saturation), protocol (network state exhaustion), and application layer (server resource exhaustion). The right mitigations differ per category.
- Volumetric mitigations can’t be done at the application level — they need CDNs, anycast routing, and scrubbing centers with more bandwidth capacity than attacker traffic. This is the domain of ISPs and CDN providers.
- Rate limiting is the first application-level defense — sliding windows are more accurate than fixed windows. Implement at multiple levels: CDN, nginx, and applications.
- Application layer DDoS is more dangerous because it looks legitimate — valid requests in large volumes, or requests to expensive endpoints, can bring servers down without large volumes.
- Caching reduces the DDoS blast radius — requests served from cache don’t touch the database. Aggressive caching on public endpoints makes systems more resilient to any traffic spike.
- Circuit breakers prevent cascade failures — when one service goes down from flooding, circuit breakers ensure incoming requests don’t keep waiting and blocking resources.
- Expensive operations must be queued — report generation, bulk exports, and heavy computation must not be processed synchronously. Attackers can exploit this with few requests.
- Timeouts for all downstream calls are mandatory — without timeouts, one slow external API can hang all threads and effectively take servers down.
- Real-time monitoring is the key to early detection — alerts for RPS anomalies, error rate spikes, and latency degradation enable responses before systems actually go down.
- Runbooks and incident response plans must be ready before attacks happen — being attacked isn’t the time to figure out what to do. Regular drills ensure teams can respond quickly and correctly.
#
- DDoS has three different categories — volumetric (bandwidth saturation), protocol (network state exhaustion), and application layer (server resource exhaustion). The right mitigations differ per category.
- Volumetric mitigations can’t be done at the application level — they need CDNs, anycast routing, and scrubbing centers with more bandwidth capacity than attacker traffic. This is the domain of ISPs and CDN providers.
- Rate limiting is the first application-level defense — sliding windows are more accurate than fixed windows. Implement at multiple levels: CDN, nginx, and applications.
- Application layer DDoS is more dangerous because it looks legitimate — valid requests in large volumes, or requests to expensive endpoints, can bring servers down without large volumes.
- Caching reduces the DDoS blast radius — requests served from cache don’t touch the database. Aggressive caching on public endpoints makes systems more resilient to any traffic spike.
- Circuit breakers prevent cascade failures — when one service goes down from flooding, circuit breakers ensure incoming requests don’t keep waiting and blocking resources.
- Expensive operations must be queued — report generation, bulk exports, and heavy computation must not be processed synchronously. Attackers can exploit this with few requests.
- Timeouts for all downstream calls are mandatory — without timeouts, one slow external API can hang all threads and effectively take servers down.
- Real-time monitoring is the key to early detection — alerts for RPS anomalies, error rate spikes, and latency degradation enable responses before systems actually go down.
- Runbooks and incident response plans must be ready before attacks happen — being attacked isn’t the time to figure out what to do. Regular drills ensure teams can respond quickly and correctly.