---
title: "Rate limiting"
description: "Rate limiting в go-kit: juju/ratelimit, token bucket на endpoint, NewErroringLimiter и NewDelayingLimiter, ErrLimited и честный 429 Too Many Requests с Retry-After."
course: "go-kit: микросервисный тулкит"
url: "https://golangmaster.ru/courses/go-kit/lessons/09-rate-limit/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 18
---

# Rate limiting

*Курс «go-kit: микросервисный тулкит», урок 9 из 25. ~18 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/go-kit/lessons/09-rate-limit/*

Три урока наблюдаемости показали, что происходит в stringsvc, — теперь научимся это ограничивать. Под нагрузкой или при дурацком клиенте сервис честнее отклонить лишнее, чем утонуть: классический ход — token bucket на endpoint'е, а готовый лимитер go kit rate limit даёт пакет ratelimit в паре с juju/ratelimit. Ты соберёшь бакет с двумя ручками — скоростью и ёмкостью, обернёшь endpoint в NewErroringLimiter или NewDelayingLimiter, проведёшь ErrLimited через кодеки из урока 05 и вернёшь клиенту 429 Too Many Requests с Retry-After. И разберёшься, чем отказ от запроса отличается от постановки в очередь.

### Зачем лимитер: fail fast вместо лавины

Сервис без ограничения входа подписывает опасный контракт: сколько пришло запросов, столько и обслужим. При всплеске трафика горутины копятся, очереди растут, латентность ползёт вверх, дедлайны из урока 04 срабатывают у всех, клиентские ретраи умножают поток — классическая лавина, из которой сервис без admission control не выходит сам. Идея лимитера проста и древняя, как телефонный коммутатор: измерить допустимый темп заранее и отклонить всё сверх него мгновенно и дёшево. Отклонённый запрос не расходует ни CPU, ни память, ни соединения с базой — его цена измеряется микросекундами и одной проверкой числа. Клиенту отказ тоже полезнее ожидания: честный 429 Too Many Requests с заголовком Retry-After говорит «притормози и вернись через секунду» вместо тишины и таймаута. Именно поэтому fail fast — основной принцип этого урока: лучше быстрый явный отказ, чем медленная деградация всех запросов сразу. Заметь, где ставится контроль: не в сервисе — домен не обязан знать о трафике, — и не в декодере, а на endpoint'е, тем самым механизмом endpoint.Middleware из урока 02, которым мы уже собирали логи, метрики и спаны. Обёртка go kit rate limit — четвёртая того же сорта, и это не совпадение: в go-kit вся механика вокруг запроса живёт в middleware.

### Token bucket: модель с двумя ручками

Алгоритм token bucket — самая ходовая модель лимитирования, потому что описывается одной метафорой. Есть бакет ёмкостью capacity: в него с постоянной скоростью капают токены — раз в fillInterval или rate токенов в секунду; каждый запрос перед проходом берёт один токен; бакет полон — запросы идут свободно, бакет пуст — запросы отклоняются. Две ручки дают точную настройку: rate задаёт среднюю скорость, которую сервис готов переваривать, capacity — допустимый всплеск: накопившийся запас токенов позволяет пропустить burst из capacity запросов подряд, а потом темп упрётся в rate. Именно эта способность «сглаживать всплески, но разрешать их» отличает token bucket от тупого счётчика «не больше N в секунду»: API, который ходит циклами, не будет резаться на ровном месте. В Go эталонная реализация — github.com/juju/ratelimit: ratelimit.NewBucket(fillInterval, capacity) капает по таймеру, ratelimit.NewBucketWithRate(rate, capacity) принимает токенов в секунду float64 и сама вычисляет внутренний квант; методы Take и WaitMaxDuration умеют брать токены неблокирующе или с потолком ожидания, Available() показывает запас, Capacity() и Rate() возвращают настройки. Бакет потокобезопасен: все методы можно звать из любых горутин — для веб-сервиса это обязательное требование, запросы приходят параллельно. Подберёшь числа ниже, а пока — как этот бакет становится middleware.

### Бакет juju/ratelimit и адаптер Allower

```go
import (
        kitratelimit "github.com/go-kit/kit/ratelimit"
        "github.com/juju/ratelimit"
)

bucket := ratelimit.NewBucketWithRate(10, 50) // 10 токенов/с, всплеск до 50

allow := kitratelimit.AllowerFunc(func() bool {
        // взять токен, только если он есть прямо сейчас; без ожидания
        return bucket.WaitMaxDuration(1, 0)
})

upper = kitratelimit.NewErroringLimiter(allow)(upper)
```

Пакет go-kit/kit/ratelimit не связан жёстко ни с одной библиотекой: NewErroringLimiter принимает интерфейс Allower { Allow() bool } — «можно ли пройти». juju/ratelimit.Bucket метода Allow не имеет, но адаптер AllowerFunc закрывает разрыв: bucket.WaitMaxDuration(1, 0) берёт один токен и возвращает true, только если он доступен немедленно; иначе токены не снимаются и вернётся false. Именно такой неблокирующей проверкой и должен жить лимитер на входе HTTP-сервиса.

### NewErroringLimiter: контракт ErrLimited

Посмотрим, что делает middleware, когда Allower сказал нет. НовЕрный вариант — вернуть клиенту что-то осмысленное — требует, чтобы отказ был типизированной ошибкой, а не строкой. Go-kit делает ровно это: пакет объявляет var ErrLimited = errors.New("rate limit exceeded") — единственный экземпляр на весь процесс, — и NewErroringLimiter возвращает его в канал err: return nil, ErrLimited. Endpoint при этом не вызывается вовсе: сервис, база, декодеры — всё остаётся нетронутым, отказ стоил одной проверки. У единого ErrLimited два следствия. Первое — проверка на принимающей стороне: в encodeError из урока 05 отказ узнаётся через errors.Is(err, kitratelimit.ErrLimited) — errors.Is устойчив к обёрткам fmt.Errorf с %w, которые обязательно появятся, когда ошибки начнут обрастать контекстом. Второе — превращение в HTTP-ответ: сырой ErrLimited с DefaultErrorEncoder из урока 04 уехал бы в 500 text/plain — формально всё работает, но клиент не отличит лимит от бага. Честный ответ — 429 Too Many Requests с Retry-After, и для этого нужен тип-носитель из арсенала уроков 04-05: ошибка, реализующая StatusCode() и Header(), через которую кодек отдаст и статус, и заголовок. Ниже соберём такой конверт, а пока зафиксируй принцип: middleware возвращает каноническую ошибку, транспортное преобразование в статус — работа кодеков, как и для ErrEmpty.

### x/time/rate: Allower и Waiter из коробки

```go
import (
        "golang.org/x/time/rate"
        kitratelimit "github.com/go-kit/kit/ratelimit"
)

limiter := rate.NewLimiter(rate.Limit(10), 50) // 10 событий/с, burst 50

upper = kitratelimit.NewErroringLimiter(limiter)(upper)  // Allow() уже есть
worker = kitratelimit.NewDelayingLimiter(limiter)(worker) // Wait(ctx) уже есть
```

golang.org/x/time/rate — вторая эталонная реализация token bucket, и она спроектирована под эти интерфейсы: type Allower interface { Allow() bool } и type Waiter interface { Wait(ctx context.Context) error } *rate.Limiter реализует сам, без адаптеров. rate.Limit(10) — токенов в секунду, 50 — burst. Одна и та же настройка обслуживает и отклоняющий, и откладывающий лимитер: выбор поведения — это выбор middleware, а не замена библиотеки.

### NewDelayingLimiter: очередь вместо отказа

NewErroringLimiter — не единственный ответ на пустой бакет, и противопоставление двух middleware пакета — важнейший выбор этого урока. NewDelayingLimiter принимает Waiter { Wait(ctx context.Context) error } и вместо отказа ждёт: limiter.Wait(ctx) блокирует горутину, пока не накапает токен, и лишь потом пропускает запрос к endpoint'у. Это уже не admission control, а throttling — сглаживание: запросы не отклоняются, а выстраиваются в невидимую очередь с темпом rate. Когда что выбирать. Для публичного HTTP API — erroring, почти всегда: горутины ждущих клиентов — это память и соединения, всплеск трафика превращается в всплеск горутин, и лимитер от одной беды защищает, создавая другую; клиенту честнее Retry-After, чем подвисший ответ. Для внутренних фоновых потоков — delaying — в самый раз: воркер, который перекачивает задачи, спокойно подождёт токен, ему некуда спешить и обидно падать; загруженная очередь — штатный режим, а не инцидент. Есть и промежуточные сценарии: WaitForN с потолком ожидания — подожди, но не дольше X, потом откажи. Обрати внимание на ctx в Wait: дедлайн из запроса уважается, и ожидание прервётся ошибкой context.DeadlineExceeded, когда клиент уже уехал. И держи в голове предельный случай delaying: burst приходящего трафика больше capacity — очередь растёт неограниченно, латентность p90 из метрик урока 07 улетает в космос, хотя ни один запрос не получил отказ. Отсутствие 429 — ещё не здоровье.

### 429 с Retry-After: конверт ошибки в кодеке

```go
type rateLimitError struct{}

func (rateLimitError) Error() string   { return "rate limit exceeded" }
func (rateLimitError) StatusCode() int { return http.StatusTooManyRequests }
func (rateLimitError) Header() http.Header {
        return http.Header{"Retry-After": []string{"1"}}
}

// в encodeError из урока 05, рядом с codeFrom:
var rl *rateLimitError
if errors.As(err, &rl) {
        w.Header().Set("Retry-After", "1")
        w.WriteHeader(http.StatusTooManyRequests)
        json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
        return
}
```

Тот же приём, что badRequestError из урока 05: тип-носитель, через который ошибка сообщает кодеку свой статус. StatusCode() даёт 429, Header() — интерфейс Headerer, который DefaultErrorEncoder и наш encodeError умеют читать: Retry-After подсказывает клиенту, через сколько вернуться. Ошибка находится через errors.As — работает и для обёрток. Теперь цепочка замкнулась: NewErroringLimiter вернул ErrLimited, endpoint-адаптер перевёл его в rateLimitError, кодек отдал честный 429.

### Куда встать в цепочке обёрток

У тебя уже четыре endpoint-middleware, и порядок их сборки в main перестал быть мелочью. Правило простое: чем дешевле отказ, тем раньше обёртка. Лог из урока 06 полезен для всех исходов, включая отказы, — он снаружи. Дальше лимитер: отклонённый запрос не должен тратить ничего, кроме строки в логе. Метрики из урока 07 и спаны из урока 08 — вопрос вкуса с важным нюансом, который станет predict-задачей ниже: если instrumentingMiddleware стоит внутри лимитера, отклонённые запросы не попадают в request_count и график нагрузки рисует только пропущенное — согласованное решение стоит задокументировать в коде, например отдельным счётчиком rate_limited_total. Трейсер из урока 08 внутрь лимитера ставить стоит: отказ 429 — тоже событие, которое хочется видеть в Jaeger, и спан на него обойдётся дешевле, чем на сервис. Итоговый порядок: loggingMiddleware снаружи, затем лимитер, затем трейсер, затем instrumentingMiddleware и endpoint. Отдельно про транспорт: ServerBefore и декодер отрабатывают до endpoint-обёрток, поэтому лимитированный запрос всё равно потратит чуть-чуть — на чтение заголовков; если этого мало, переноси лимит в ServerBefore как RequestFunc, но тогда он перестанет быть endpoint-логикой и потеряет транспортную независимость. Для stringsvc честная цена в декодере — копейки, endpoint-обёртка правильнее.

### Шесть стратегий лимитирования

| Стратегия | Что ограничивает | Ответ при превышении | Когда уместна |
| --- | --- | --- | --- |
| Token bucket на endpoint | темп одного метода | 429 + Retry-After | канон go-kit: у каждого endpoint свой бакет в main |
| Общий бакет на сервис | суммарный темп | 429 + Retry-After | простая защита всех методов одной квотой |
| NewDelayingLimiter | темп через очередь | ответ ждёт токен | фоновые воркеры, внутренние вызовы |
| Per-IP карта бакетов | темп каждого клиента | 429 + Retry-After | публичные API; нужна своя обёртка с картой и чисткой |
| Per-API-key | квоты тарифных планов | 429 + Retry-After | платные API; бакеты живут по ключу клиента |
| Лимит на балансировщике | входящий поток целиком | 429 на edge | защита до сервиса; настраивает не Go-код |

### Per-endpoint, per-IP и честные числа

Бакет в примере выше один и обслуживает endpoint целиком — это канон go-kit и правильная точка старта: у каждого endpoint свои ресурсы и свой допустимый темп. Но числа не берутся с потолка, и вот честная методика. Средний rate бери из метрик урока 07: если p90 латентности приемлема при 8 запросах в секунду, ставь 10 с запасом; capacity считай как допустимый всплеск — сколько запросов подряд сервис переварит без деградации: для быстрых in-memory методов это десятки, для походов в базу — единицы. Пер-клиентские лимиты — отдельная дисциплина: juju/ratelimit не знает про IP и ключи, поэтому per-IP лимитер — это твоя обёртка над endpoint'ом: карта «ключ клиента → свой бакет», mutex вокруг неё, чистка давних записей по таймеру, иначе карта растёт вечно, — всё как в любом лимитере на HTTP-фреймворке. Ключ честно брать из заголовков с осторожностью: X-Forwarded-For подделывается, а NAT складывает сотни людей в один адрес — per-IP на публичном API всегда компромисс, реальный идентификатор клиента — аутентификация, о которой пойдёт речь в уроке про безопасность. И ещё одна честная деталь: бакет с rate 10 пропускает ровно 10 токенов в секунду в среднем, но не гарантирует равномерность — заполнение квантуется, и burst может прийти в одну миллисекунду; если нужна строгая равномерность, это уже задача leaky bucket или планировщика, а не token bucket.

### Наблюдаемость лимитера: покажи, что сработал

Лимитер, который молча режет трафик, хуже отсутствие лимитера: деградация клиента будет выглядеть загадкой. Поэтому отказ — это событие, и его нужно сделать видимым во всех трёх слоях, которые ты уже построил. В логах из урока 06: оберни лимитер в endpoint-middleware, которая при errors.Is(err, kitratelimit.ErrLimited) пишет logger.Log("msg", "rate limited", "method", ...) — или доверь это loggingMiddleware, если он снаружи и видит err: там же появится и took, показывающий, что отказ стоил микросекунды. В метриках из урока 07: объяви kitprometheus.NewCounterFrom с метками method и ограниченным набором исходов — limited/allowed — и инкрементируй его в той же обёртке; алерт на rate этого счётчика расскажет об атаке или кривом клиенте раньше, чем расскажет недовольный заказчик. Полезен и gauge Available() бакета: запас токенов, который изнутри сервиса показывает, насколько близко ты к отказам, — Set каждый период и дашборд готов. В трейсе из урока 08: спан лимитированного запроса обязан существовать — поэтому трейсер и стоит внутри лимитера; событие в спане с текстом ошибки появится само, если обёртка вернёт err в канал. Такая связка даёт полный цикл: метрика позвала — трейс показал, где — лог объяснил, кто. Без неё ты узнаешь о работе лимитера только из писем пользователей.

> **Лимитер — не аутентификация и не защита от всего.** Rate limiting решает одну задачу — ограничить темп, и его не стоит нагружать чужими: он не отличает атаку от живого клиента, не заменяет авторизацию и не спасает от одного тяжёлого запроса, который один отклонённый темп и превысил. Per-IP лимиты врут за NAT и подделываются через X-Forwarded-For. Настоящая граница доверия ставится аутентификацией и валидацией входа, а лимитер — грубый, честный и очень дешёвый предохранитель поверх всего этого.

> **Честно о запуске кода.** Урок требует установленного Go и модулей github.com/go-kit/kit, github.com/juju/ratelimit и golang.org/x/time/rate — в песочнице курса сторонние модули не исполняются, поэтому интерактивы построены на чтении кода. Локально: собери stringsvc с лимитером и кодеком 429, дёрни /uppercase двадцать раз подряд — увидишь, как после двадцати первых ответов начнут приходить 429 с Retry-After, а затем прогони ab или hey на минуту и посмотри на счётчик отказов.

### Пять правил rate limiting

1. Лимит на endpoint'е: rate.NewLimiter или адаптер Allower над бакетом juju/ratelimit, сборка в main рядом с остальными middleware.
2. Публичный API — NewErroringLimiter: мгновенный отказ вместо очереди из горутин; delaying — для фоновых внутренних потоков.
3. ErrLimited проверяется errors.Is, а в HTTP превращается типом-носителем: 429 Too Many Requests с Retry-After через Headerer.
4. Числа из метрик: rate — по среднему темпу с запасом, capacity — по допустимому всплеску; gauge Available() покажет запас токенов.
5. Отказ — событие: счётчик rate_limited_total, строка в логе, спан в трейсе; молчащий лимитер хуже отсутствующего.

**Вопрос:** Бакет пуст, NewErroringLimiter получил false от Allower. Что вернёт обёрнутый endpoint?

- Блокируется, пока не появится токен
- ✅ (nil, ErrLimited): endpoint и сервис не вызываются вовсе
- Пустой response и nil err: клиент получает 200
- Панику: пустой бакет — ошибка конфигурации

NewErroringLimiter — это fail fast: неблокирующая проверка Allow вернула false, middleware сразу возвращает nil и каноническую ошибку ErrLimited из пакета ratelimit. Сервис, декодеры и база не трогаются — в этом смысл admission control. Клиент увидит 429 только потому, что encodeError из уроков 04-05 перевёл ошибку в статус; с дефолтным энкодером это был бы 500 text/plain.

**Вопрос:** Почему для внешнего HTTP API выбирают NewErroringLimiter, а не NewDelayingLimiter?

- DelayingLimiter медленнее в миллисекундах на запрос
- ✅ Очередь ждущих клиентов копит горутины и соединения — всплеск трафика превращается в всплеск памяти
- DelayingLimiter не уважает context.Context
- ErroringLimiter умеет в Retry-After, а delaying — нет

Каждый отложенный запрос — заблокированная горутина с открытым соединением: burst в десять тысяч запросов превращается в десять тысяч ожидающих горутин, лимитер от одной беды защищает, создавая другую. Отказ 429 с Retry-After дешевле для сервера и полезнее для клиента. Причина не в скорости самих middleware и не в Retry-After — заголовок пишет кодек в обоих случаях; несимметрия в том, что у erroring-отказа клиенту есть что читать сразу. Контекст delaying уважает: Wait(ctx) прервётся по дедлайну.

**Вопрос:** encodeError должен узнать лимит-отказ. Какая проверка правильная?

- err.Error() == "rate limit exceeded" — сравнение строк
- ✅ errors.Is(err, kitratelimit.ErrLimited) — сравнение с каноническим значением
- err == http.StatusTooManyRequests — сравнение со статусом
- Никакой: DefaultErrorEncoder сам отличит лимит от прочих ошибок

Сравнение строк — хрупко: текст поменяется, и отказ замаскируется под 500. errors.Is сравнивает с тем самым экземпляром ErrLimited, который вернул middleware, — и остаётся корректным, если ошибку обернули fmt.Errorf("...: %w", err). Это тот же канон type-носителей и обёрток, что и errors.As для badRequestError в уроке 05: middleware говорит на языке ошибок, кодек — на языке статусов, между ними — errors.

### Токен не взят: что с Available()?

```go
bucket := ratelimit.NewBucketWithRate(10, 50)
bucket.Wait(50) // вычерпали всё

// через 40 мс (токены ещё не натекли):
allow := kitratelimit.AllowerFunc(func() bool {
        return bucket.WaitMaxDuration(1, 0)
})
ok := allow.Allow() // false

// вопрос: Available() до и после Allow()?
```

**Что выведет программа?**

- Уменьшился: попытка забирает долю токена
- ✅ Не изменился: неудачная попытка не снимает токены
- Стал отрицательным: долги токенов учитываются
- Подскочил: Allow прогнала таймер наполнения

Контракт juju/ratelimit точен: WaitMaxDuration берёт токены, только если время ожидания не превышает maxWait, иначе «the operation does not take any tokens and returns false». Максимальное ожидание ноль — токенов нет, снять нечего, Available() остался прежним. Отказ в token bucket не наказывает клиента долей токена: следующая попытка через интервал наполнения имеет тот же шанс, что и предыдущая.

### Порядок обёрток: растёт ли request_count при 429?

```go
// порядок A:
upper = loggingMiddleware(logger)(upper)
upper = kitratelimit.NewErroringLimiter(allow)(upper)
upper = instrumentingMiddleware(requestCount, requestLatency)(upper)

// порядок B:
upper = loggingMiddleware(logger)(upper)
upper = instrumentingMiddleware(requestCount, requestLatency)(upper)
upper = kitratelimit.NewErroringLimiter(allow)(upper)

// бакет пуст, клиент шлёт запрос
```

**Что выведет программа?**

- Одинаково: в обоих порядках request_count растёт
- Только A: в B отклонённый запрос не доходит до метрик
- ✅ Только B: в A метрики стоят снаружи лимитера
- Ни в одном: err всегда ломает счётчик

В порядке B лимитер снаружи метрик: отклонённый запрос разворачивается раньше, чем instrumentingMiddleware увидела вызов, — request_count не растёт, график показывает только пропущенный трафик. В порядке A метрики внутри: они честно насчитают 429 как вызов с error=true, потому что err дошёл до их defer. Оба варианта легитимны, но смыслы разные — «сколько обслужили» против «сколько пришло»; выбери осознанно и запиши в комментарии main, а отказы дополнительно покажи отдельным счётчиком rate_limited_total.

### Собери лимитер и его кодек

Восстанови сборку. Пропуски — по порядку: конструктор бакета juju/ratelimit, адаптер функции под интерфейс go-kit, erroring-middleware, каноническая ошибка пакета и статус-код для константы http.

```go
bucket := ratelimit.___(10, 50) // 10 токенов/с, burst 50

allow := kitratelimit.___(func() bool {
        return bucket.WaitMaxDuration(1, 0)
})
upper = kitratelimit.___(allow)(upper)

// в encodeError:
if errors.Is(err, kitratelimit.___) {
        w.WriteHeader(http.___)
}
```

Подсказка: Первый — фабрика бакета с rate и ёмкостью; второй и третий — из пакета ratelimit go-kit; четвёртый — единая ошибка пакета; пятый — 429.

Правильные ответы в пропусках: `NewBucketWithRate`, `AllowerFunc`, `NewErroringLimiter`, `ErrLimited`, `StatusTooManyRequests`

### Путь отклонённого запроса

Расставь путь лимитированного запроса по порядку: от прихода на порт до Retry-After у клиента.

Правильный порядок строк:

1. `POST /uppercase пришёл на ListenAndServe, mux нашёл Server`
2. `loggingMiddleware снаружи зафиксировала begin`
3. `NewErroringLimiter вызвала Allower.Allow()`
4. `bucket.WaitMaxDuration(1, 0) вернула false: бакет пуст`
5. `middleware вернула (nil, ErrLimited); endpoint и сервис не вызывались`
6. `loggingMiddleware записала err и took в лог`
7. `encodeError нашёл rateLimitError через errors.As и записал 429 с Retry-After`
8. `клиент прочитал Retry-After и притих на секунду`

### Термины урока

- **juju/ratelimit.Bucket** — Token bucket: NewBucketWithRate(rate, capacity); WaitMaxDuration(1, 0) — неблокирующая попытка взять токен; Available/Capacity/Rate; потокобезопасен.
- **kitratelimit.Allower** — Интерфейс { Allow() bool } для NewErroringLimiter; AllowerFunc — адаптер функции; *rate.Limiter из x/time/rate реализует сам.
- **NewErroringLimiter** — Endpoint.Middleware: Allow() false → (nil, ErrLimited), endpoint не вызывается; fail fast для внешнего HTTP.
- **NewDelayingLimiter** — Endpoint.Middleware поверх Waiter { Wait(ctx) error }: запрос ждёт токен — очередь вместо отказа; для фоновых воркеров; уважает дедлайны ctx.
- **ErrLimited** — Единая ошибка пакета ratelimit: «rate limit exceeded»; проверяется errors.Is; без тип-носителя уедет в 500 — нужен 429.
- **Retry-After** — Заголовок ответа 429/503: сколько клиенту подождать; в go-kit ставится через Header() у типа-носителя из урока 05.

### Что дальше

Лимитер встал в строй: token bucket с двумя ручками, неблокирующая проверка на endpoint'е, каноническая ErrLimited и честный 429 с Retry-After в кодеке из урока 05. Оборачивания из уроков [02-endpoint](/courses/go-kit/lessons/02-endpoint/) и [04-http-transport](/courses/go-kit/lessons/04-http-transport/) снова показали универсальность: ни сервис, ни транспорт не узнали о появлении лимита. Освежить кодеки ошибок — в [уроке 05-codeki](/courses/go-kit/lessons/05-codeki/), счётчики и гейджи — в [уроке 07-metriki](/courses/go-kit/lessons/07-metriki/), карту курса — на [странице курса](/courses/go-kit/). Но лимитер защищает сервис от клиентов, а кто защитит клиентов от зависимостей? Если соседний сервис лёг, твой прокси будет честно ждать таймауты на каждом вызове и копить горутины. Дальше — урок 10-circuit-breaker: предохранитель на endpoint'е, состояния closed/open/half-open и мгновенный отказ, пока зависимость чинят.

## Коротко: что запомнить

- Token bucket: rate — средний темп, capacity — допустимый всплеск; juju/ratelimit.NewBucketWithRate(10, 50) — потокобезопасный бакет.
- go-kit ratelimit принимает интерфейсы: NewErroringLimiter(Allower) отклоняет с ErrLimited, NewDelayingLimiter(Waiter) ждёт токен.
- juju-бакет не умеет Allow — адаптер AllowerFunc с bucket.WaitMaxDuration(1, 0) даёт неблокирующую проверку; x/time/rate реализует оба интерфейса сам.
- ErrLimited проверяется errors.Is; HTTP-лицо отказа — тип-носитель из урока 05: StatusCode() 429 и Header() с Retry-After.
- Для внешнего API — erroring (fail fast), для фоновых потоков — delaying (очередь); burst больше capacity в delaying = бесконечный рост латентности.
- Порядок обёрток: лог снаружи, лимитер раньше тяжёлых middleware; отклонённые запросы в request_count не попадают — покажи их отдельным счётчиком.
- Числа — из метрик: rate по среднему темпу, capacity по всплеску; gauge Available() и алерт на rate_limited_total закрывают наблюдаемость.

## Домашнее задание

Добавь в stringsvc полный контур лимитирования. Первое: собери два бакета в main — для /uppercase 10 токенов в секунду с ёмкостью 50, для /count 2 в секунду с ёмкостью 10 — через juju/ratelimit.NewBucketWithRate и адаптер AllowerFunc с WaitMaxDuration(1, 0). Второе: оберни каждый endpoint в kitratelimit.NewErroringLimiter, поставь обёртку после loggingMiddleware и до instrumentingMiddleware; прокомментируй в main, почему именно так, с оглядкой на predict про порядок. Третье: сделай rateLimitError с StatusCode() 429 и Header() Retry-After: 1, подключи его в encodeError рядом с badRequestError из урока 05, проверь errors.Is(err, kitratelimit.ErrLimited) ветку. Четвёртое: объяви счётчик rate_limited_total через kitprometheus.NewCounterFrom с меткой method и инкрементируй его в обёртке при отказе; смонтируй gauge Available() и собери запрос Prometheus для доли отказов. Пятое: прогони hey на минуту и опиши, как соотносятся request_count и rate_limited_total; затем замени лимитер /count на NewDelayingLimiter с rate.NewLimiter и сравни поведение по p90 из гистограммы урока 07 — объясни, почему 429 исчезли, а латентность выросла.

## Вопросы и ответы по уроку

### С чего начать подбор rate и capacity?

Rate — из среднего трафика с запасом: посмотри rate(request_count[5m]) из урока 07 на пике и добавь двадцать-тридцать процентов. Capacity — это допустимый burst: сколько запросов подряд endpoint переварит без деградации латентности; быстрым методам хватит десятков, походам в базу — единиц. После запуска корректируй по gauge Available(): если запас нулевой в моменты пиковой нагрузки — rate занижен.

### Чем x/time/rate отличается от juju/ratelimit?

Обе реализации token bucket, разница в API и экосистеме: x/time/rate — официальный supplementary-пакет, его Limiter реализует интерфейсы go-kit Allower и Waiter напрямую; juju/ratelimit даёт более тонкий набор (Take, TakeMaxDuration, TakeAvailable, часы-квант) и требует адаптера. Для типовой задачи с go-kit удобнее x/time/rate; juju хорош, когда нужны неблокирующие Take с задержкой в ответе.

### Лимит на endpoint или на весь сервис?

Начни с per-endpoint: у методов разные ресурсы и стоимость, общий бакет позволит горячему endpoint съесть квоту соседей. Общий лимит имеет смысл как вторая линия — грубый предохранитель от суммарной перегрузки, когда даже легитимные потоки вместе опасны. Per-IP и per-key — отдельные обёртки с картой бакетов и чисткой; они нужны, когда трафик неоднороден по клиентам.
