Rate Limiting
Protect your API from abuse with rate limiting.
Overview
Rate limiting restricts the number of requests a client can make within a time window. This prevents:
- DoS attacks
- Brute force attempts
- API abuse
- Resource exhaustion
Basic Rate Limiter
Memory-Based Limiter
type RateLimiter struct {
requests map[string]*RateLimit
mutex sync.RWMutex
limit int
window time.Duration
}
type RateLimit struct {
Count int
ExpiresAt time.Time
}
func NewRateLimiter(limit int, window time.Duration) *RateLimiter {
return &RateLimiter{
requests: make(map[string]*RateLimit),
limit: limit,
window: window,
}
}
func (r *RateLimiter) IsAllowed(key string) bool {
r.mutex.Lock()
defer r.mutex.Unlock()
now := time.Now()
// Get or create rate limit
rl, exists := r.requests[key]
if !exists || now.After(rl.ExpiresAt) {
r.requests[key] = &RateLimit{
Count: 1,
ExpiresAt: now.Add(r.window),
}
return true
}
// Check limit
if rl.Count >= r.limit {
return false
}
rl.Count++
return true
}
func (r *RateLimiter) Remaining(key string) int {
r.mutex.RLock()
defer r.mutex.RUnlock()
rl, exists := r.requests[key]
if !exists {
return r.limit
}
if time.Now().After(rl.ExpiresAt) {
return r.limit
}
return r.limit - rl.Count
}
Redis-Based Rate Limiter
For distributed systems:
type RedisRateLimiter struct {
kv *kv.Client `inject:""`
limit int
window time.Duration
}
func NewRedisRateLimiter(limit int, window time.Duration) *RedisRateLimiter {
return &RedisRateLimiter{
limit: limit,
window: window,
}
}
func (r *RedisRateLimiter) IsAllowed(key string) (bool, int, time.Time) {
rateKey := "ratelimit:" + key
// Increment counter
count, _ := r.kv.Incr(rateKey)
// Set expiry on first request
if count == 1 {
r.kv.Expire(rateKey, r.window)
}
// Get TTL for reset time
ttl, _ := r.kv.TTL(rateKey)
resetAt := time.Now().Add(ttl)
remaining := r.limit - int(count)
if remaining < 0 {
remaining = 0
}
return count <= int64(r.limit), remaining, resetAt
}
Rate Limit Middleware
Basic Middleware
type RateLimitMiddleware struct {
limiter *RateLimiter
}
func NewRateLimitMiddleware(limit int, window time.Duration) *RateLimitMiddleware {
return &RateLimitMiddleware{
limiter: NewRateLimiter(limit, window),
}
}
func (m *RateLimitMiddleware) Handle(ctx types.Context, next types.Next) any {
// Get client identifier
key := m.getClientKey(ctx)
// Check rate limit
if !m.limiter.IsAllowed(key) {
return ctx.Status(429).JSON(map[string]string{
"error": "Too many requests",
})
}
return next()
}
func (m *RateLimitMiddleware) getClientKey(ctx types.Context) string {
// Try authenticated user first
if userID := ctx.Get("user_id"); userID != nil {
return "user:" + userID.(string)
}
// Fall back to IP address
return "ip:" + ctx.IP()
}
With Headers
func (m *RateLimitMiddleware) Handle(ctx types.Context, next types.Next) any {
key := m.getClientKey(ctx)
allowed, remaining, resetAt := m.limiter.Check(key)
// Set rate limit headers
ctx.SetHeader("X-RateLimit-Limit", strconv.Itoa(m.limiter.limit))
ctx.SetHeader("X-RateLimit-Remaining", strconv.Itoa(remaining))
ctx.SetHeader("X-RateLimit-Reset", strconv.FormatInt(resetAt.Unix(), 10))
if !allowed {
retryAfter := int(time.Until(resetAt).Seconds())
ctx.SetHeader("Retry-After", strconv.Itoa(retryAfter))
return ctx.Status(429).JSON(map[string]interface{}{
"error": "Too many requests",
"retry_after": retryAfter,
})
}
return next()
}
Rate Limit Strategies
By IP Address
func ByIP() KeyExtractor {
return func(ctx types.Context) string {
return "ip:" + ctx.IP()
}
}
By User
func ByUser() KeyExtractor {
return func(ctx types.Context) string {
if userID := ctx.Get("user_id"); userID != nil {
return "user:" + userID.(string)
}
return "ip:" + ctx.IP()
}
}
By API Key
func ByAPIKey() KeyExtractor {
return func(ctx types.Context) string {
apiKey := ctx.Header("X-API-Key")
if apiKey != "" {
return "apikey:" + apiKey
}
return "ip:" + ctx.IP()
}
}
By Endpoint
func ByEndpoint() KeyExtractor {
return func(ctx types.Context) string {
return "endpoint:" + ctx.Method() + ":" + ctx.Path() + ":ip:" + ctx.IP()
}
}
Configurable Rate Limiter
type RateLimitConfig struct {
Limit int
Window time.Duration
KeyFunc func(ctx types.Context) string
SkipFunc func(ctx types.Context) bool
ErrorFunc func(ctx types.Context, limit int, remaining int, resetAt time.Time) any
}
func DefaultRateLimitConfig() *RateLimitConfig {
return &RateLimitConfig{
Limit: 100,
Window: time.Minute,
KeyFunc: func(ctx types.Context) string {
return ctx.IP()
},
SkipFunc: nil,
ErrorFunc: func(ctx types.Context, limit int, remaining int, resetAt time.Time) any {
return ctx.Status(429).JSON(map[string]string{
"error": "Too many requests",
})
},
}
}
type ConfigurableRateLimiter struct {
config *RateLimitConfig
limiter *RateLimiter
}
func (m *ConfigurableRateLimiter) Handle(ctx types.Context, next types.Next) any {
// Check skip function
if m.config.SkipFunc != nil && m.config.SkipFunc(ctx) {
return next()
}
key := m.config.KeyFunc(ctx)
allowed, remaining, resetAt := m.limiter.Check(key)
// Set headers
ctx.SetHeader("X-RateLimit-Limit", strconv.Itoa(m.config.Limit))
ctx.SetHeader("X-RateLimit-Remaining", strconv.Itoa(remaining))
ctx.SetHeader("X-RateLimit-Reset", strconv.FormatInt(resetAt.Unix(), 10))
if !allowed {
return m.config.ErrorFunc(ctx, m.config.Limit, remaining, resetAt)
}
return next()
}
Different Limits for Different Routes
func (c *Controller) Routes() types.Routes {
// Strict limit for login (prevent brute force)
loginLimit := NewRateLimitMiddleware(5, time.Minute)
// Normal limit for API
apiLimit := NewRateLimitMiddleware(100, time.Minute)
// Higher limit for authenticated users
authLimit := NewRateLimitMiddleware(1000, time.Minute)
return types.Routes{
{Method: "POST", Path: "/auth/login", Handler: c.Login,
Middlewares: []any{loginLimit}},
{Method: "GET", Path: "/api/public", Handler: c.PublicData,
Middlewares: []any{apiLimit}},
{Method: "GET", Path: "/api/users", Handler: c.Users,
Middlewares: []any{&AuthMiddleware{}, authLimit}},
}
}
Tiered Rate Limits
Different limits for different user tiers:
type TieredRateLimiter struct {
limiters map[string]*RateLimiter
}
func NewTieredRateLimiter() *TieredRateLimiter {
return &TieredRateLimiter{
limiters: map[string]*RateLimiter{
"free": NewRateLimiter(100, time.Hour),
"basic": NewRateLimiter(1000, time.Hour),
"pro": NewRateLimiter(10000, time.Hour),
"enterprise": NewRateLimiter(100000, time.Hour),
},
}
}
func (t *TieredRateLimiter) Handle(ctx types.Context, next types.Next) any {
// Get user tier
tier := ctx.Get("user_tier")
if tier == nil {
tier = "free"
}
limiter, ok := t.limiters[tier.(string)]
if !ok {
limiter = t.limiters["free"]
}
key := ctx.Get("user_id").(string)
if !limiter.IsAllowed(key) {
return ctx.Status(429).JSON(map[string]string{
"error": "Rate limit exceeded for your plan",
})
}
return next()
}
Sliding Window Algorithm
More accurate rate limiting:
type SlidingWindowLimiter struct {
kv *kv.Client
limit int
window time.Duration
}
func (l *SlidingWindowLimiter) IsAllowed(key string) bool {
now := time.Now()
windowStart := now.Add(-l.window)
// Use sorted set with timestamp scores
rateKey := "ratelimit:" + key
// Remove old entries
l.kv.ZRemRangeByScore(rateKey, "-inf", strconv.FormatInt(windowStart.UnixNano(), 10))
// Count current entries
count, _ := l.kv.ZCard(rateKey)
if count >= int64(l.limit) {
return false
}
// Add new entry
l.kv.ZAdd(rateKey, float64(now.UnixNano()), uuid.New().String())
l.kv.Expire(rateKey, l.window)
return true
}
Best Practices
- Use Redis for distributed rate limiting
- Set appropriate limits based on your capacity
- Include rate limit headers in responses
- Use different limits for different endpoints
- Authenticate before rate limiting where possible
- Log rate limit violations for monitoring
- Provide clear error messages with retry information
- Consider retry-after header for better client experience
Common Configurations
| Use Case | Limit | Window |
|---|---|---|
| Login attempts | 5 | 15 minutes |
| Password reset | 3 | 1 hour |
| API (free tier) | 100 | 1 hour |
| API (paid tier) | 1000 | 1 hour |
| File uploads | 10 | 1 hour |
| Search queries | 30 | 1 minute |
Next Steps
- Authentication - Secure your API
- CORS - Cross-origin configuration
- Security Overview - Security best practices