Skip to content
Back to skills

1726 Rate Limiting 282926f0

ASecurity

`golang.org/x/time/rate` implements a token bucket rate limiter:

  • 9 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added June 1, 2026
developmentrustgoapi

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned June 1, 2026

npx -y skills add tools-only/X-Skills --skill 1726-rate-limiting_282926f0 --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of 1726 Rate Limiting 282926f0?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for 1726 Rate Limiting 282926f0
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/tools-only-1726-rate-limiting-282926f0/badge)](https://www.skillsdirectory.com/skills/tools-only-1726-rate-limiting-282926f0)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
# Rate Limiting

## Token Bucket Algorithm

`golang.org/x/time/rate` implements a token bucket rate limiter:

- A bucket holds up to **burst** tokens
- Tokens are added at a rate of **rps** (requests per second)
- Each request consumes one token
- If no tokens are available, the request is rejected (or waits)

Example: `rate.NewLimiter(10, 20)` allows 10 requests/second sustained with bursts up to 20.

## Global Rate Limiting

Protect the entire service from being overwhelmed:

```go
// Rate limit middleware using x/time/rate
func RateLimit(rps float64, burst int) func(http.Handler) http.Handler {
    limiter := rate.NewLimiter(rate.Limit(rps), burst)

    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            if !limiter.Allow() {
                w.Header().Set("Retry-After", "1")
                http.Error(w, "rate limit exceeded", http.StatusTooManyRequests)
                return
            }
            next.ServeHTTP(w, r)
        })
    }
}

// Usage
mux := http.NewServeMux()
mux.HandleFunc("GET /api/data", s.handleGetData)
handler := RateLimit(100, 200)(mux) // 100 rps, burst of 200
```

## Per-IP Rate Limiting

Prevent individual clients from monopolizing resources:

```go
// Per-IP rate limiting
type IPRateLimiter struct {
    mu       sync.RWMutex
    limiters map[string]*rate.Limiter
    rps      float64
    burst    int
}

func NewIPRateLimiter(rps float64, burst int) *IPRateLimiter {
    return &IPRateLimiter{
        limiters: make(map[string]*rate.Limiter),
        rps:      rps,
        burst:    burst,
    }
}

func (l *IPRateLimiter) GetLimiter(ip string) *rate.Limiter {
    l.mu.RLock()
    limiter, exists := l.limiters[ip]
    l.mu.RUnlock()

    if exists {
        return limiter
    }

    l.mu.Lock()
    defer l.mu.Unlock()

    // Double-check after acquiring write lock
    if limiter, exists = l.limiters[ip]; exists {
        return limiter
    }

    limiter = rate.NewLimiter(rate.Limit(l.rps), l.burst)
    l.limiters[ip] = limiter
    return limiter
}
```

### Per-IP Middleware

```go
func PerIPRateLimit(rps float64, burst int, trustProxy bool) func(http.Handler) http.Handler {
    limiter := NewIPRateLimiter(rps, burst)

    // Start cleanup goroutine
    go limiter.cleanup(5 * time.Minute)

    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            ip := extractIP(r, trustProxy)
            if !limiter.GetLimiter(ip).Allow() {
                w.Header().Set("Retry-After", "1")
                http.Error(w, "rate limit exceeded", http.StatusTooManyRequests)
                return
            }
            next.ServeHTTP(w, r)
        })
    }
}

// extractIP returns the client IP address from the request.
// WARNING: X-Forwarded-For and X-Real-IP headers can be spoofed by clients.
// Only trust these headers when behind a known reverse proxy that strips/overwrites them.
func extractIP(r *http.Request, trustProxy bool) string {
    if trustProxy {
        // Check X-Forwarded-For for proxied requests
        if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
            // Take the first IP (client IP)
            if idx := strings.Index(xff, ","); idx != -1 {
                return strings.TrimSpace(xff[:idx])
            }
            return strings.TrimSpace(xff)
        }
        // Check X-Real-IP
        if xri := r.Header.Get("X-Real-IP"); xri != "" {
            return xri
        }
    }
    // Fall back to RemoteAddr
    host, _, err := net.SplitHostPort(r.RemoteAddr)
    if err != nil {
        return r.RemoteAddr
    }
    return host
}
```

## Cleaning Up Stale Limiters

Without cleanup, the limiter map grows indefinitely. Remove entries that haven't been used recently:

```go
type trackedLimiter struct {
    limiter  *rate.Limiter
    lastSeen time.Time
}

type IPRateLimiter struct {
    mu       sync.RWMutex
    limiters map[string]*trackedLimiter
    rps      float64
    burst    int
}

func (l *IPRateLimiter) GetLimiter(ip string) *rate.Limiter {
    l.mu.RLock()
    tracked, exists := l.limiters[ip]
    l.mu.RUnlock()

    if exists {
        // Update last seen time under write lock
        l.mu.Lock()
        tracked.lastSeen = time.Now()
        l.mu.Unlock()
        return tracked.limiter
    }

    l.mu.Lock()
    defer l.mu.Unlock()

    // Double-check
    if tracked, exists = l.limiters[ip]; exists {
        tracked.lastSeen = time.Now()
        return tracked.limiter
    }

    limiter := rate.NewLimiter(rate.Limit(l.rps), l.burst)
    l.limiters[ip] = &trackedLimiter{
        limiter:  limiter,
        lastSeen: time.Now(),
    }
    return limiter
}

func (l *IPRateLimiter) cleanup(maxAge time.Duration) {
    ticker := time.NewTicker(maxAge)
    defer ticker.Stop()

    for range ticker.C {
        l.mu.Lock()
        cutoff := time.Now().Add(-maxAge)
        for ip, tracked := range l.limiters {
            if tracked.lastSeen.Before(cutoff) {
                delete(l.limiters, ip)
            }
        }
        l.mu.Unlock()
    }
}
```

## Rate Limiting by API Key or User

For authenticated endpoints, rate limit by user identity instead of IP:

```go
type KeyRateLimiter struct {
    mu       sync.RWMutex
    limiters map[string]*trackedLimiter
    tiers    map[string]Tier // API key -> tier
}

type Tier struct {
    RPS   float64
    Burst int
}

var defaultTiers = map[string]Tier{
    "free":       {RPS: 10, Burst: 20},
    "pro":        {RPS: 100, Burst: 200},
    "enterprise": {RPS: 1000, Burst: 2000},
}

func (l *KeyRateLimiter) GetLimiter(apiKey string) *rate.Limiter {
    l.mu.RLock()
    tracked, exists := l.limiters[apiKey]
    l.mu.RUnlock()

    if exists {
        l.mu.Lock()
        tracked.lastSeen = time.Now()
        l.mu.Unlock()
        return tracked.limiter
    }

    l.mu.Lock()
    defer l.mu.Unlock()

    if tracked, exists = l.limiters[apiKey]; exists {
        tracked.lastSeen = time.Now()
        return tracked.limiter
    }

    tier, ok := l.tiers[apiKey]
    if !ok {
        tier = defaultTiers["free"]
    }

    limiter := rate.NewLimiter(rate.Limit(tier.RPS), tier.Burst)
    l.limiters[apiKey] = &trackedLimiter{
        limiter:  limiter,
        lastSeen: time.Now(),
    }
    return limiter
}
```

### API Key Middleware

```go
func APIKeyRateLimit(keyLimiter *KeyRateLimiter) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            apiKey := r.Header.Get("X-API-Key")
            if apiKey == "" {
                http.Error(w, "missing API key", http.StatusUnauthorized)
                return
            }

            limiter := keyLimiter.GetLimiter(apiKey)
            if !limiter.Allow() {
                w.Header().Set("Retry-After", "1")
                w.Header().Set("X-RateLimit-Limit", fmt.Sprintf("%.0f", float64(limiter.Limit())))
                w.Header().Set("X-RateLimit-Remaining", "0")
                http.Error(w, "rate limit exceeded", http.StatusTooManyRequests)
                return
            }

            next.ServeHTTP(w, r)
        })
    }
}
```

## Returning Proper 429 Responses

Always include informative headers when rejecting rate-limited requests:

```go
func rateLimitResponse(w http.ResponseWriter, limiter *rate.Limiter) {
    reservation := limiter.Reserve()
    if !reservation.OK() {
        http.Error(w, "rate limit exceeded", http.StatusTooManyRequests)
        return
    }

    delay := reservation.Delay()
    reservation.Cancel() // We're rejecting, not waiting

    w.Header().Set("Retry-After", fmt.Sprintf("%.0f", delay.Seconds()+1))
    w.Header().Set("X-RateLimit-Limit", fmt.Sprintf("%.0f", float64(limiter.Limit())))
    w.Header().Set("X-RateLimit-Remaining", "0")

    w.WriteHeader(http.StatusTooManyRequests)
    json.NewEncoder(w).Encode(map[string]any{
        "error":       "rate limit exceeded",
        "retry_after": delay.Seconds(),
    })
}
```

## Combining Rate Limiters

Layer global and per-IP limits for defense in depth:

```go
mux := http.NewServeMux()
mux.HandleFunc("GET /api/data", s.handleGetData)

// Apply per-IP first (inner), then global (outer)
handler := RateLimit(1000, 2000)(          // global: 1000 rps
    PerIPRateLimit(10, 20)(                // per-IP: 10 rps
        mux,
    ),
)
```

This ensures no single IP can use more than 10 rps, while the service overall caps at 1000 rps.

Files in this skill

  • README.md935 B
  • skill.md8.4 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.