Дата: 2026-06-11 Статус: Accepted (требует реализации)
Текущий rate limiter в backend/internal/middleware/ratelimit.go использует in-memory golang.org/x/time/rate. В main.go:54 создаётся глобальный лимитер на 60 req/min на IP.
In-memory rate limiter не работает в production при:
Использовать Redis-backed rate limiter с библиотекой github.com/ulule/limiter/v3.
// internal/middleware/ratelimit_redis.go
package middleware
import (
"context"
"net/http"
"time"
"github.com/redis/go-redis/v9"
"github.com/ulule/limiter/v3"
"github.com/ulule/limiter/v3/drivers/store/redis"
"github.com/go-chi/chi/v5/middleware"
)
type RedisRateLimiter struct {
instance *limiter.Limiter
keyFunc func(*http.Request) string
}
func NewRedisRateLimiter(redisClient *redis.Client, rate limiter.Rate, keyFunc func(*http.Request) string) *RedisRateLimiter {
store := redis.NewStoreWithOptions(redisClient, limiter.StoreOptions{Prefix: "ratelimit"})
instance := limiter.New(store, rate)
return &RedisRateLimiter{instance: instance, keyFunc: keyFunc}
}
func (r *RedisRateLimiter) Middleware() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
key := r.keyFunc(req)
ctx := context.Background()
limitCtx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
defer cancel()
limit, err := r.instance.Get(limitCtx, key)
if err != nil {
// Fail open или fail closed — решение бизнеса
// Рекомендую fail open с логированием
next.ServeHTTP(w, req)
return
}
w.Header().Set("X-RateLimit-Limit", strconv.FormatInt(limit.Limit, 10))
w.Header().Set("X-RateLimit-Remaining", strconv.FormatInt(limit.Remaining, 10))
w.Header().Set("X-RateLimit-Reset", strconv.FormatInt(limit.Reset, 10))
if limit.Reached {
writeRateLimitError(w, limit.Reset)
return
}
next.ServeHTTP(w, req)
})
}
}
// В main.go — настройка разных лимитов для разных групп
func setupRateLimiters(r *chi.Mux, redisClient *redis.Client) {
// Auth endpoints: 10/min по IP
authLimiter := NewRedisRateLimiter(redisClient, limiter.Rate{Period: time.Minute, Limit: 10}, func(r *http.Request) string {
return "auth:" + middleware.GetIP(r)
})
r.Group(func(r chi.Router) {
r.Use(authLimiter.Middleware())
r.Post("/auth/register", ...)
r.Post("/auth/login", ...)
})
// API read: 60/min по UserID (если авторизован) или IP
apiReadLimiter := NewRedisRateLimiter(redisClient, limiter.Rate{Period: time.Minute, Limit: 60}, func(r *http.Request) string {
if userID := middleware.GetUserID(r.Context()); userID != "" {
return "api_read:user:" + userID
}
return "api_read:ip:" + middleware.GetIP(r)
})
r.Group(func(r chi.Router) {
r.Use(apiReadLimiter.Middleware())
r.Get("/places", ...)
r.Get("/services", ...)
})
// API write: 10/min по UserID
apiWriteLimiter := NewRedisRateLimiter(redisClient, limiter.Rate{Period: time.Minute, Limit: 10}, func(r *http.Request) string {
return "api_write:user:" + middleware.GetUserID(r.Context())
})
r.Group(func(r chi.Router) {
r.Use(apiWriteLimiter.Middleware())
r.Post("/places", ...)
r.Post("/services", ...)
})
}
| Вариант | Почему отклонен |
|---|---|
| nginx rate limiting | Нет per-user лимитов, меньше гибкости, сложнее тестировать |
| Cloudflare / API Gateway | Vendor lock-in, стоимость, задержка конфигурации |
| In-memory с sticky sessions | Антипаттерн, ломает масштабируемость |
Положительные:
X-RateLimit-*Отрицательные:
#decision-record #rate-limiting #redis #scaling #architecture