Rate Limiter
Go-библиотека для ограничения частоты запросов (rate limiting) с поддержкой 5 классических алгоритмов. Использует Redis и Lua-скрипты для атомарных операций в распределённой среде.
Зачем нужен Rate Limiter
Проблемы без Rate Limiting
- Неравномерная нагрузка — «шумные соседи» потребляют львиную долю ресурсов и мешают другим клиентам
- Перерасход ресурсов — бесконтрольное потребление API ведёт к лишним затратам
- Каскадные отказы — один клиент может перегрузить сервис и вызвать падение для всех
- DDoS-атаки — вредоносный трафик перегружает систему
Преимущества Rate Limiting
- Предсказуемость — стабильная производительность для всех пользователей
- Экономия — контроль использования платных API и инфраструктуры
- Справедливое распределение — квоты для каждого клиента (IP, user ID, API key)
- Защита сервисов — предотвращение перегрузки и отказоустойчивость
Подробнее о rate limiting в системном дизайне: Rate Limiter — System Design Space
Реализованные алгоритмы
Библиотека поддерживает 5 классических алгоритмов rate limiting. Выбор зависит от требований к точности, памяти и поведению при всплесках трафика.
| Алгоритм |
Когда использовать |
Память |
Burst |
Точность |
| Token Bucket |
Допустимы кратковременные всплески |
2 числа на ключ |
✓ |
Высокая |
| Leaky Bucket |
Нужен равномерный поток запросов |
2 числа на ключ |
✗ |
Высокая |
| Fixed Window |
Простые лимиты, допустима boundary burst |
1 число на ключ |
На границе окна |
±100% на границе |
| Sliding Window Log |
Максимальная точность, меньше ограничений по памяти |
O(N) записей |
✗ |
Высокая |
| Sliding Window Counter |
Баланс точности и памяти |
1 число на ключ |
Частичный |
Хорошая |
Token Bucket
Ведро с токенами пополняется с постоянной скоростью. Каждый запрос забирает один токен.
Плюсы: Эффективность по памяти (2 числа на пользователя), позволяет burst-трафик, простая реализация.
Минусы: Race conditions в distributed без атомарности (решено Lua-скриптами), сложно подобрать размер bucket.
Подходит для: API с допустимыми кратковременными всплесками (например, 100 req/min с burst до 20).
Leaky Bucket
Запросы «протекают» через условное отверстие с постоянной скоростью. Сглаживает трафик.
Плюсы: Равномерный поток запросов, защита от burst.
Минусы: Может замедлять легитимных пользователей при всплесках.
Подходит для: Систем, где важна равномерность нагрузки (очереди обработки, защита downstream-сервисов).
Fixed Window Counter
Счётчик запросов в фиксированном временном окне (например, минута). На границе окна сбрасывается.
Плюсы: Минимальная память, очень быстрая реализация (INCR в Redis).
Минусы: Boundary burst — в конце одного окна и начале другого можно сделать до 2× лимита запросов.
Подходит для: Простых лимитов, где допустима некоторая погрешность на границах окон.
Sliding Window Log
Хранит timestamp каждого запроса в sorted set. Окно «скользит» вместе с временем.
Плюсы: Максимальная точность, нет boundary burst.
Минусы: Память O(N), где N — количество запросов в окне.
Подходит для: Строгих лимитов, когда точность важнее памяти.
Sliding Window Counter
Гибрид: использует приближение на основе текущего и предыдущего окна. Один счётчик в Redis.
Плюсы: Хороший баланс точности и памяти (1 ключ на пользователя).
Минусы: Аппроксимация — не идеальная точность.
Подходит для: Production-сценариев с большим числом ключей, когда нужен компромисс точности и памяти.
Возможности
- 5 алгоритмов: Token Bucket, Leaky Bucket, Fixed Window, Sliding Window Log, Sliding Window Counter
- Распределённый режим — Redis + Lua-скрипты для атомарности
- Множественные лимиты (например, 100 req/min и 1000 req/hour для одного ключа)
- Информативный результат:
Allowed, Remaining, RetryAfter
- Минимальные зависимости: только Redis client
Требования
Установка
go get github.com/Rinsvent/rate-limiter
Быстрый старт
package main
import (
"context"
"log"
"time"
"github.com/Rinsvent/rate-limiter"
"github.com/redis/go-redis/v9"
)
func main() {
rdb := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
defer rdb.Close()
limiter, err := ratelimiter.NewLimiter("token-bucket", rdb, []ratelimiter.Rate{
{Duration: time.Minute, Value: 100},
})
if err != nil {
log.Fatal(err)
}
ctx := context.Background()
result, err := limiter.Allow(ctx, "user:123")
if err != nil {
log.Fatal(err)
}
if !result.Allowed {
log.Printf("Rate limit exceeded. Retry after %v", result.RetryAfter)
return
}
log.Printf("Request allowed. Remaining: %d", result.Remaining)
}
API
Создание лимитера
limiter, err := ratelimiter.NewLimiter(algorithm, redisClient, rates)
Алгоритмы: "token-bucket", "leaky-bucket", "fixed-window-counter", "sliding-window-log", "sliding-window-counter"
Rates — массив лимитов (применяются по AND: запрос должен пройти все):
rates := []ratelimiter.Rate{
{Duration: time.Minute, Value: 100}, // 100 req/min
{Duration: time.Hour, Value: 1000}, // 1000 req/hour
}
Проверка запроса
result, err := limiter.Allow(ctx, key)
- key — идентификатор (IP, user ID, API key и т.д.)
- Result.Allowed — разрешён ли запрос
- Result.Remaining — оставшиеся запросы в текущем окне
- Result.RetryAfter — сколько ждать до повторной попытки (если Allowed=false)
HTTP-интеграция
Для ответа 429 Too Many Requests:
if !result.Allowed {
w.Header().Set("Retry-After", strconv.Itoa(int(result.RetryAfter.Seconds())))
http.Error(w, "Too Many Requests", http.StatusTooManyRequests)
return
}
w.Header().Set("X-RateLimit-Remaining", strconv.Itoa(result.Remaining))
Примеры
# Redis должен быть запущен (docker compose up -d)
go run ./examples/basic
Docker
docker compose up -d
# Redis на localhost:6379
Тесты
# Требуется Redis на localhost:6379
make test
# Покрытие
make test-coverage
Лицензия
MIT — см. LICENSE.
Ссылки
Contributing
Приветствуются pull request'ы! Подробнее: CONTRIBUTING.md
Перед отправкой:
- Запустите
make test
- Добавьте тесты для новой функциональности
- Соблюдайте стиль кода (gofmt)
Как внести вклад