Frontend Labs
⋯

Day 92 — Rate Limiting Algorithms Deep Dive

Learning objective Implement all 5 major rate limiting algorithms; understand their behavior under burst traffic
Key packages sync, sync/atomic, time, math
Security context Correct algorithm choice determines whether DDoS traffic is blocked cleanly or leaks through

Key Concepts

Algorithm Memory Burst Boundary issue Best for
Fixed window O(1) Yes Double-rate at boundary Simple API limits
Sliding window log O(n) per key No None Accurate audit
Sliding window counter O(1) Partial Slight approximation DDoS (scalable)
Token bucket O(1) Yes (up to B) None API platforms (Stripe, AWS)
Leaky bucket O(1) No None Traffic smoothing

Exercise — All 5 Algorithms

package ratelimit

import (
    "sync"
    "sync/atomic"
    "time"
)

// 1. Fixed Window — fast but vulnerable to boundary bursts
type FixedWindow struct {
    mu        sync.Mutex
    limit     int
    count     int
    windowEnd time.Time
    period    time.Duration
}

func NewFixedWindow(limit int, period time.Duration) *FixedWindow {
    return &FixedWindow{limit: limit, period: period, windowEnd: time.Now().Add(period)}
}

func (fw *FixedWindow) Allow() bool {
    fw.mu.Lock()
    defer fw.mu.Unlock()
    now := time.Now()
    if now.After(fw.windowEnd) {
        fw.count = 0
        fw.windowEnd = now.Add(fw.period)
    }
    if fw.count >= fw.limit {
        return false
    }
    fw.count++
    return true
}

// 2. Sliding Window Log — accurate, memory-heavy (stores each request timestamp)
type SlidingWindowLog struct {
    mu      sync.Mutex
    limit   int
    period  time.Duration
    entries []time.Time
}

func NewSlidingWindowLog(limit int, period time.Duration) *SlidingWindowLog {
    return &SlidingWindowLog{limit: limit, period: period}
}

func (swl *SlidingWindowLog) Allow() bool {
    swl.mu.Lock()
    defer swl.mu.Unlock()
    now := time.Now()
    cutoff := now.Add(-swl.period)

    // Evict old entries
    valid := swl.entries[:0]
    for _, t := range swl.entries {
        if t.After(cutoff) {
            valid = append(valid, t)
        }
    }
    swl.entries = valid

    if len(swl.entries) >= swl.limit {
        return false
    }
    swl.entries = append(swl.entries, now)
    return true
}

// 3. Sliding Window Counter — approximation using two fixed windows
type SlidingWindowCounter struct {
    mu          sync.Mutex
    limit       int
    period      time.Duration
    curCount    int
    prevCount   int
    windowStart time.Time
}

func NewSlidingWindowCounter(limit int, period time.Duration) *SlidingWindowCounter {
    return &SlidingWindowCounter{limit: limit, period: period, windowStart: time.Now()}
}

func (swc *SlidingWindowCounter) Allow() bool {
    swc.mu.Lock()
    defer swc.mu.Unlock()
    now := time.Now()
    elapsed := now.Sub(swc.windowStart)

    if elapsed >= swc.period {
        // Shift windows
        swc.prevCount = swc.curCount
        swc.curCount = 0
        swc.windowStart = now
        elapsed = 0
    }

    // Weighted count: prev window contribution + current window
    ratio := 1.0 - elapsed.Seconds()/swc.period.Seconds()
    estimated := float64(swc.prevCount)*ratio + float64(swc.curCount)
    if int(estimated) >= swc.limit {
        return false
    }
    swc.curCount++
    return true
}

// 4. Token Bucket — allows burst up to capacity B, refills at rate R tokens/sec
type TokenBucket struct {
    mu       sync.Mutex
    tokens   float64
    capacity float64
    rate     float64 // tokens per second
    lastTime time.Time
}

func NewTokenBucket(capacity, rate float64) *TokenBucket {
    return &TokenBucket{
        tokens:   capacity,
        capacity: capacity,
        rate:     rate,
        lastTime: time.Now(),
    }
}

func (tb *TokenBucket) Allow() bool {
    tb.mu.Lock()
    defer tb.mu.Unlock()
    now := time.Now()
    elapsed := now.Sub(tb.lastTime).Seconds()
    tb.lastTime = now

    tb.tokens = min(tb.capacity, tb.tokens+elapsed*tb.rate)
    if tb.tokens < 1.0 {
        return false
    }
    tb.tokens--
    return true
}

func min(a, b float64) float64 {
    if a < b {
        return a
    }
    return b
}

// 5. Leaky Bucket — smooth constant outflow regardless of input burst
type LeakyBucket struct {
    capacity  int64 // queue depth
    current   atomic.Int64
    drainRate time.Duration // time between each drain
    done      chan struct{}
}

func NewLeakyBucket(capacity int64, drainRate time.Duration) *LeakyBucket {
    lb := &LeakyBucket{capacity: capacity, drainRate: drainRate, done: make(chan struct{})}
    go lb.drain()
    return lb
}

func (lb *LeakyBucket) drain() {
    ticker := time.NewTicker(lb.drainRate)
    defer ticker.Stop()
    for {
        select {
        case <-lb.done:
            return
        case <-ticker.C:
            for {
                cur := lb.current.Load()
                if cur <= 0 {
                    break
                }
                if lb.current.CompareAndSwap(cur, cur-1) {
                    break
                }
            }
        }
    }
}

func (lb *LeakyBucket) Allow() bool {
    for {
        cur := lb.current.Load()
        if cur >= lb.capacity {
            return false
        }
        if lb.current.CompareAndSwap(cur, cur+1) {
            return true
        }
    }
}

func (lb *LeakyBucket) Stop() { close(lb.done) }

Real-World Context

  • AWS API Gateway uses token bucket (burst capacity + sustained rate).
  • Stripe uses sliding window counter per API key.
  • DDoS mitigation layers prefer token bucket at L4 (per-IP) and sliding window counter at L7 (per path/endpoint) because token bucket allows legitimate burst while bounding steady-state rate.

Generated from week13.md · 8 Oct 2026 16:36