Frontend Labs
⋯

Day 21 — time Package

Learning Objectives

  • Build a sliding-window rate limiter — the algorithm used by AWS API Gateway and Stripe
  • Use time.NewTicker vs time.Tick (the latter leaks)
  • Understand monotonic clock vs wall clock in Go

Key Concepts

// time.After — creates a timer channel, fires once
// WARNING: leaks if you don't receive from it
select {
case <-time.After(5 * time.Second):
    return errors.New("timeout")
case result := <-ch:
    return result
}

// time.NewTicker — reusable ticker, MUST Stop() to prevent leak
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for range ticker.C {
    // runs every second
}

// time.Tick — convenience wrapper, LEAKS (can't stop)
// Only use for package-level tickers that run forever
for range time.Tick(time.Second) { }  // OK at top-level, bad in functions

// Monotonic clock: time.Since, time.Until use it automatically
// Wall clock: time.Now().Format(...), time.Parse use it
start := time.Now()
// ... work ...
elapsed := time.Since(start) // uses monotonic — correct even across DST changes

Exercise: Sliding-Window Rate Limiter

package main

import (
    "fmt"
    "sync"
    "time"
)

// SlidingWindowLimiter implements the sliding window counter algorithm.
// More accurate than fixed-window at boundary conditions.
type SlidingWindowLimiter struct {
    mu         sync.Mutex
    limit      int           // max requests per window
    window     time.Duration // window size
    buckets    map[string]*windowState
    cleanupTick *time.Ticker
}

type windowState struct {
    count     int
    windowEnd time.Time
    prevCount int       // count from previous window (for sliding approximation)
    prevEnd   time.Time
}

func NewSlidingWindowLimiter(limit int, window time.Duration) *SlidingWindowLimiter {
    l := &SlidingWindowLimiter{
        limit:       limit,
        window:      window,
        buckets:     make(map[string]*windowState),
        cleanupTick: time.NewTicker(window * 2),
    }
    go l.cleanup()
    return l
}

func (l *SlidingWindowLimiter) Allow(key string) bool {
    l.mu.Lock()
    defer l.mu.Unlock()

    now := time.Now()
    state, ok := l.buckets[key]
    if !ok {
        state = &windowState{windowEnd: now.Add(l.window)}
        l.buckets[key] = state
    }

    // Rotate window if expired
    if now.After(state.windowEnd) {
        state.prevCount = state.count
        state.prevEnd = state.windowEnd
        state.count = 0
        state.windowEnd = now.Add(l.window)
    }

    // Sliding window: weighted sum of current + fraction of previous window
    elapsed := now.Sub(state.prevEnd)
    prevWeight := 1.0 - float64(elapsed)/float64(l.window)
    if prevWeight < 0 { prevWeight = 0 }

    estimated := float64(state.prevCount)*prevWeight + float64(state.count)
    if estimated >= float64(l.limit) {
        return false
    }

    state.count++
    return true
}

func (l *SlidingWindowLimiter) cleanup() {
    for range l.cleanupTick.C {
        l.mu.Lock()
        cutoff := time.Now().Add(-l.window * 2)
        for key, state := range l.buckets {
            if state.windowEnd.Before(cutoff) {
                delete(l.buckets, key)
            }
        }
        l.mu.Unlock()
    }
}

func (l *SlidingWindowLimiter) Stop() { l.cleanupTick.Stop() }

func main() {
    limiter := NewSlidingWindowLimiter(10, time.Second)
    defer limiter.Stop()

    clientIP := "192.168.1.1"
    allowed, blocked := 0, 0

    for i := 0; i < 25; i++ {
        if limiter.Allow(clientIP) {
            allowed++
        } else {
            blocked++
        }
    }
    fmt.Printf("Burst test: %d allowed, %d blocked (limit: 10)\n", allowed, blocked)

    // Wait for window to reset
    time.Sleep(1100 * time.Millisecond)
    allowed, blocked = 0, 0
    for i := 0; i < 5; i++ {
        if limiter.Allow(clientIP) {
            allowed++
        } else {
            blocked++
        }
    }
    fmt.Printf("After reset: %d allowed, %d blocked\n", allowed, blocked)
}

Real-World Context

This rate limiter is the foundation of Day 76 (per-API-key token bucket) and Day 93 (per-IP DDoS mitigation). The sliding window algorithm avoids the "double burst" problem of fixed windows — an attacker can't send 100% of limit at the end of window 1 + 100% at the start of window 2. AWS API Gateway uses exactly this approach.


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