Building a Real-Time DDoS Detection Engine in Go: A Practical Guide
Every production web application faces the same threat: abnormal traffic spikes that can bring your service to its knees. While enterprise-grade DDoS protection exists, understanding how detection works under the hood makes you a better engineer—and Go's concurrency model makes it surprisingly approachable.
In this tutorial, we'll build a lightweight anomaly detection engine that watches HTTP traffic in real-time and flags suspicious patterns. You'll learn practical techniques for rate limiting, statistical anomaly detection, and concurrent data processing.
Understanding DDoS Detection Fundamentals
DDoS detection isn't about catching every malicious request—it's about identifying patterns that deviate from normal behavior. Three key signals matter:
Request rate anomalies: Sudden spikes in requests per second from a single IP or globally. A legitimate traffic surge happens gradually; attacks spike instantly.
Geographic clustering: 10,000 requests from a single /24 subnet in 60 seconds isn't organic traffic—it's likely a botnet.
Behavioral signatures: Repeated 404s, identical User-Agent strings across IPs, or requests with no referrer headers all indicate automated attacks rather than human browsing.
Our Go implementation will focus on the first two signals using sliding window counters and threshold-based detection.
Building the Traffic Monitor
Start with a data structure to track requests efficiently. Go's sync.Map provides thread-safe access without explicit locking:
package main
import (
"sync"
"time"
)
type TrafficMonitor struct {
ipRequests sync.Map // map[string]*RequestCounter
globalCount *RequestCounter
threshold int
window time.Duration
}
type RequestCounter struct {
mu sync.Mutex
timestamps []time.Time
}
func NewTrafficMonitor(threshold int, window time.Duration) *TrafficMonitor {
return &TrafficMonitor{
globalCount: &RequestCounter{},
threshold: threshold,
window: window,
}
}
The RequestCounter maintains a slice of timestamps for each IP. When checking for anomalies, we'll filter out timestamps older than our detection window (typically 60 seconds).
Implementing Sliding Window Detection
The core detection logic uses a sliding window: count requests within the last N seconds and trigger alerts when thresholds break:
func (tm *TrafficMonitor) RecordRequest(ip string) bool {
now := time.Now()
// Get or create counter for this IP
val, _ := tm.ipRequests.LoadOrStore(ip, &RequestCounter{})
counter := val.(*RequestCounter)
counter.mu.Lock()
defer counter.mu.Unlock()
// Remove stale timestamps outside our window
cutoff := now.Add(-tm.window)
counter.timestamps = filterTimestamps(counter.timestamps, cutoff)
// Add current request
counter.timestamps = append(counter.timestamps, now)
// Check if threshold exceeded
if len(counter.timestamps) > tm.threshold {
return true // Anomaly detected
}
return false
}
func filterTimestamps(timestamps []time.Time, cutoff time.Time) []time.Time {
result := timestamps[:0]
for _, t := range timestamps {
if t.After(cutoff) {
result = append(result, t)
}
}
return result
}
This approach is memory-efficient because we automatically prune old data. For production systems handling millions of requests, consider using a ring buffer or switching to Redis with TTL-based expiry.
Integrating with HTTP Middleware
Wrap your existing HTTP handlers with detection middleware:
func (tm *TrafficMonitor) Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ip := extractIP(r)
if tm.RecordRequest(ip) {
// Log the anomaly
log.Printf("ALERT: Potential DDoS from %s", ip)
// Optional: Rate limit or block
http.Error(w, "Too Many Requests", http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}
func extractIP(r *http.Request) string {
// Check X-Forwarded-For header first (if behind proxy)
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
return strings.Split(xff, ",")[0]
}
return strings.Split(r.RemoteAddr, ":")[0]
}
Statistical Enhancement: Z-Score Detection
Fixed thresholds work but can produce false positives during legitimate traffic spikes (product launches, viral posts). Add statistical anomaly detection using z-scores:
func (tm *TrafficMonitor) CalculateZScore(currentRate float64) float64 {
// Maintain rolling average and standard deviation
// If z-score > 3.0, current rate is 3 standard deviations above mean
mean := tm.calculateMean()
stdDev := tm.calculateStdDev(mean)
if stdDev == 0 {
return 0
}
return (currentRate - mean) / stdDev
}
When z-score > 3, you're seeing a statistically significant deviation—likely an attack rather than normal variance.
Production Considerations
This implementation teaches core concepts, but production systems need additional layers:
Distributed detection: In multi-server deployments, aggregate metrics in Redis or a time-series database like Prometheus. Single-server detection misses distributed attacks.
False positive handling: Whitelist known IPs (health checkers, monitoring tools, APIs). Implement progressive rate limiting rather than hard blocks.
Memory bounds: Set maximum IP entries tracked simultaneously. Use LRU eviction to prevent memory exhaustion from IP rotation attacks.
Key Takeaways
Building a DDoS detector demystifies traffic analysis and teaches valuable Go patterns: concurrent map access with sync.Map, time-window algorithms, and HTTP middleware composition. While you'll likely use Cloudflare or AWS Shield in production, understanding detection mechanics helps you tune those tools intelligently and build complementary application-layer protections.
The complete working example demonstrates that effective security monitoring doesn't require complex machine learning—solid fundamentals, Go's concurrency primitives, and statistical thinking get you surprisingly far.
Start small: implement basic rate limiting, monitor your metrics, then iterate with statistical enhancements as you learn your traffic patterns. The best DDoS protection is the one you actually understand.