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.
#

← Previous: Remote Code Execution   Next: Brute Force

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact