# Decision Record: Redis-based Rate Limiting **Дата**: 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** при: - Горизонтальном масштабировании (multiple replicas) - Рестартах контейнеров (сброс счётчиков) - Балансировке нагрузки (разные IP на разных инстансах) ## Решение **Использовать Redis-backed rate limiter с библиотекой `github.com/ulule/limiter/v3`.** ### Почему ulule/limiter: - Поддерживает Redis, in-memory, Memcached - Sliding window / token bucket алгоритмы - Атомарные операции через Lua-скрипты - Гибкие ключи (IP, UserID, custom) - Готовые middleware для Chi, Gin, Echo, stdlib ### Реализация ```go // 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** | Антипаттерн, ломает масштабируемость | ## Последствия **Положительные:** - Работает в кластере - Гибкие правила per endpoint / per user / per IP - Стандартные заголовки `X-RateLimit-*` - Атомарность через Redis Lua **Отрицательные:** - Зависимость от доступности Redis (fail open стратегия) - Небольшая латентность (~1-2ms) - Нужно мониторить Redis память ## Связанные заметки - [[backend-rate-limiting]] - [[deploy-production-readiness]] — Redis HA для прод - [[architecture-overview]] ## Теги #decision-record #rate-limiting #redis #scaling #architecture