---
title: "Timeout и Throttle: защита от перегрузки"
description: "middleware.Timeout в chi ставит дедлайн хендлеру и отвечает 504, Throttle ограничивает параллелизм и отвечает 503. Таймауты go: механика и порядок слоёв."
course: "Chi: роутер для Go HTTP-сервисов"
url: "https://golangmaster.ru/courses/chi/lessons/13-timeout-throttle/"
published: "2026-09-19"
updated: "2026-09-27"
minutes: 18
---

# Timeout и Throttle: защита от перегрузки

*Курс «Chi: роутер для Go HTTP-сервисов», урок 13 из 25. ~18 минут. Обновлено 27.09.2026. Источник: https://golangmaster.ru/courses/chi/lessons/13-timeout-throttle/*

Медленная база данных и всплеск трафика роняют сервис на Go надёжнее любой опечатки, и ровно на этот случай стоит связка chi timeout throttle: дедлайн на каждый хендлер и лимит одновременных запросов. Разберём 504 от middleware.Timeout, очередь Throttle и ThrottleBacklog, механику ctx.Done() и внешний барьер http.Server.

Хендлеры из прошлых уроков отвечали мгновенно: карта заметок в памяти, никакой сети, никакой базы. В реальном сервисе самое дорогое место — запрос к БД, и когда она залипает, каждый входящий запрос прирастает одной висящей горутиной. Пятьдесят запросов — пятьдесят горутин, триста — триста, и память процесса растёт быстрее, чем дежурный успевает открыть дашборд. От этой болезни два лекарства, и оба — middleware из пакета chi/middleware. middleware.Timeout ставит дедлайн на каждый хендлер и по истечении возвращает клиенту 504. middleware.Throttle считает одновременные запросы и всё сверх лимита встречает 503 сразу, без ожидания. В этом уроке — механика дедлайна: почему хендлер не прерывают силой и что значит следить за r.Context().Done(); жёсткий и мягкий лимит параллелизма; место предохранителей в цепочке и их связь с таймаутами http.Server.

### middleware.Timeout: дедлайн для каждого хендлера

middleware.Timeout(duration) оборачивает всю дальнейшую цепочку таймером: middleware запускает её в отдельной горутине и сам ждёт — либо хендлер закончится, либо истечёт срок. Во втором случае клиенту уходит 504 Gateway Timeout, а контекст запроса гаснет через context.WithTimeout на r.Context(). Код ответа выбран не случайно: 504 по спецификации HTTP означает, что время ожидания истекло — ровно то, что произошло; 503 в chi уже занят Throttle-ом, и смешивать эти два отказа не нужно: мониторинг по ним различает перегрузку и дедлайн. Уровень важен не меньше кода: Timeout ставится одним r.Use и выдаёт дедлайн каждому хендлеру ниже по цепочке, а не выборочно. В связке chi timeout throttle это первое кольцо защиты — второе про количество, о нём ниже. Словарь тот же, что в [уроке про context.Context](/courses/go-advanced/lessons/09-context/): Done(), Err(), Deadline() — обычные таймеры контекста, только включённые на весь роутер.

### Timeout в цепочке

```go
package main

import (
        "net/http"
        "time"

        "github.com/go-chi/chi/v5"
        "github.com/go-chi/chi/v5/middleware"
)

func main() {
        r := chi.NewRouter()

        r.Use(middleware.Logger)
        r.Use(middleware.Recoverer)
        r.Use(middleware.Timeout(2 * time.Second)) // дедлайн на каждый хендлер

        r.Get("/report", reportHandler)

        http.ListenAndServe(":8080", r)
}
```

Одна строка r.Use(middleware.Timeout(2 * time.Second)) выдаёт каждому хендлеру ниже двухсекундный дедлайн. По истечении chi отдаёт 504 Gateway Timeout и отменяет контекст запроса — при этом сам хендлер, как увидим дальше, не прерывается силой. Ставить Timeout имеет смысл после Recoverer: слой оборачивает всё ниже по цепочке, и никакой маршрут не останется без дедлайна. Проверить код можно только локальным Go: песочница сайта TCP-сервер не поднимает.

### Хендлер не прерывают силой: r.Context().Done()

Главное, что нужно понять про chi timeout: дедлайн не убивает хендлер. В Go нет механизма выстрелить горутину насквозь — runtime не отнимает управление. Когда дедлайн истёк, middleware просто пишет клиенту 504, а ваш хендлер продолжает жить в своей горутине и делает свою работу, пусть её результат уже никому не достанется. Middleware лишь включает сигнал: канал r.Context().Done() закрывается, Err() начинает возвращать context.DeadlineExceeded. Хендлер обязан сам следить за этим сигналом — через select вокруг долгих операций — и, что важнее, прокидывать контекст вниз: в запросы к БД и внешним API. Тогда дедлайн доезжает до самого медленного места, запрос отменяется там, и горутина освобождается по-настоящему. Хендлер без этой дисциплины формально «поддерживает» таймауты, а фактически работает до конца — правильная форма выглядит так.

### Хендлер, который слушает ctx.Done()

```go
type reportResult struct {
        Rows []string
}

func reportHandler(w http.ResponseWriter, r *http.Request) {
        ctx := r.Context()

        done := make(chan reportResult, 1)
        go func() {
                res := buildReport() // медленная работа: десятки секунд
                done <- res
        }()

        select {
        case <-ctx.Done():
                // дедлайн middleware.Timeout истёк или клиент закрыл соединение
                log.Println("запрос брошен:", ctx.Err())
                return
        case res := <-done:
                json.NewEncoder(w).Encode(res)
        }
}
```

Классическая форма хендлера, дружащего с дедлайном: работа уходит в горутину, а хендлер стоит в select между ctx.Done() и каналом с результатом. Как только Timeout отменяет контекст, Done() закрывается и хендлер возвращается — клиенту между тем уже улетел 504. Канал сделан буферизованным на 1: если хендлер ушёл в ветку отмены, горутина с результатом не зависнет на отправке, а сложит значение в буфер и спокойно завершится.

### Проверка: код ответа по дедлайну

**Вопрос:** Дедлайн middleware.Timeout истёк. Какой код ответа получит клиент?

- 503 Service Unavailable
- ✅ 504 Gateway Timeout
- 500 Internal Server Error
- 408 Request Timeout

chi.Timeout возвращает 504 Gateway Timeout — семантика «срок ожидания истёк». 503 в chi занят Throttle-ом, 500 — про ошибку в коде, а 408 — про медленного клиента на этапе приёма запроса, это территория серверных таймаутов, а не middleware.

### Почему долгая горутина дорабатывает до конца

Пусть хендлер отработал по-честному: увидел закрытый Done() и вернулся. Но внутри него была запущена горутина с buildReport, а она контекста не получает и про 504 не знает. Результат её труда уже никому не нужен, но она доработает до конца — потратит процессор, память и соединение с базой. Это не баг, а цена модели Go: горутину нельзя убить со стороны, с ней можно только договориться через канал или контекст. Отсюда правило: каждую запущенную горутину снабжайте контекстом — buildReport(ctx) вместо buildReport() — и внутри неё тем же select следите за Done(). Второй нюанс: если к моменту пробуждения select готово несколько случаев — и Done() закрыт, и результат уже лежит в канале, — Go выбирает среди готовых случайно, поэтому в ветке с результатом первым делом проверяйте ctx.Err(). Больше про устройство горутин — в [уроке про горутины](/courses/go-advanced/lessons/04-gorutiny/).

> **Как умирает сервис без таймаутов.** Типичный производственный инцидент: ночью БД залипает на реконструкции индекса, таймауты в коде не настроены, и каждый запрос к API прирастает горутиной, которая стоит в ожидании соединения. Пятьдесят запросов — пятьдесят горутин, каждая тянет буферы и выделенную память. Через час дежурный видит ползущий вверх график RSS, потом OOM-киллера в dmesg, потом пустой монитор вместо сайта. Перезапуск возвращает сервис — но при залипшей базе сценарий повторится, и так до утра. Предохранители из этого урока рвут цепочку раньше: Timeout задаёт каждому запросу срок, Throttle — предел по количеству, и до OOM дело не доходит.

### Throttle и ThrottleBacklog: мгновенный отказ или очередь

Второй предохранитель смотрит не на время, а на количество. middleware.Throttle(n) держит счётчик одновременных запросов: пока работают не больше n — всё проходит, как только n-е место занято, следующий запрос получает 503 Service Unavailable мгновенно, не вставая в очередь. Это предохранитель от толпы: толпа всё равно не обслужится, и честнее отказать сразу, чем копить висящие соединения. Мягкая версия — middleware.ThrottleBacklog(limit, backlog, timeout): до limit запросов работают, до backlog ждут в очереди не дольше timeout, переполнение очереди или просрочка ожидания дают тот же 503. Разница для клиента ощутимая: короткий всплеск ThrottleBacklog переживёт ожиданием, и лишь настоящий затор кончается отказом. Оба middleware — из того же пакета chi/middleware, что Logger и Recoverer из [урока про встроенные слои](/courses/chi/lessons/06-vstroennye-middleware/), и оба возвращают именно 503, а не 504.

### Throttle и ThrottleBacklog в коде

```go
r := chi.NewRouter()

// жёсткий лимит: работают до 10 запросов одновременно,
// 11-й и далее сразу получают 503 Service Unavailable
r.Use(middleware.Throttle(10))

// мягкая альтернатива: 20 работают, до 50 ждут в очереди
// не дольше 5 секунд; переполнение очереди и просрочка
// ожидания тоже дают 503
// r.Use(middleware.ThrottleBacklog(20, 50, 5*time.Second))

r.Get("/report", reportHandler)
```

Два предохранителя одного семейства. Throttle(10) — мгновенный отказ: запрос либо сразу идёт в работу, либо сразу получает 503, никакой очереди. ThrottleBacklog(limit, backlog, timeout) добавляет буфер ожидания: краткий всплеск из пяти лишних запросов успеет дождаться освободившихся слотов, а долгий затор всё равно кончится 503. Включать оба сразу не надо — внешний Throttle перехватит запросы раньше, чем очередь увидит их. Запустите локальным Go и снимите нагрузку утилитой вроде hey: статусы видно в логе.

### Что напечатает select с истёкшим дедлайном?

```go
package main

import (
        "context"
        "fmt"
        "time"
)

func main() {
        ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
        defer cancel()

        ch := make(chan string)
        go func() {
                time.Sleep(200 * time.Millisecond)
                ch <- "готово"
        }()

        select {
        case res := <-ch:
                fmt.Println(res)
        case <-ctx.Done():
                fmt.Println(ctx.Err())
        }
}
```

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

- готово
- ✅ context deadline exceeded
- программа зависнет навсегда
- panic: concurrent send

Таймер в 50 миллисекунд истекает раньше, чем горутина успевает отправить результат, — select берёт сработавший случай ctx.Done() и печатает context deadline exceeded. А горутина-отправитель остаётся висеть на отправке в небуферизованный канал навсегда: получателя больше нет. Это ровно та горутина-зомби, о которой был блок про долгую работу.

### Проверка: Throttle под нагрузкой

**Вопрос:** Сервис обёрнут в middleware.Throttle(10), и одновременно пришло 25 запросов. Что получит запрос номер 15?

- Встанет в очередь и подождёт освободившийся слот
- ✅ 503 Service Unavailable сразу, без ожидания
- 504 Gateway Timeout через заданное время
- Выполнится, но со статусом 206 Partial Content

Throttle — жёсткий лимит без очереди: сверх n одновременных идёт немедленный 503. Очередь ожидания с таймаутом — это уже ThrottleBacklog(limit, backlog, timeout): он держит лишние запросы в backlog, пока не переполнится или не истечёт срок.

### Соберите предохранители

Впишите имена двух middleware: один отвечает за дедлайн и 504, второй — за лимит параллелизма и 503.

```go
r := chi.NewRouter()

r.Use(middleware.Recoverer)
r.Use(middleware.___(2 * time.Second)) // дедлайн на каждый хендлер
r.Use(middleware.___(8))               // не больше 8 одновременных

r.Get("/report", reportHandler)
```

Подсказка: Один смотрит на часы, второй — на счётчик активных запросов.

Правильные ответы в пропусках: `Timeout`, `Throttle`

### Куда ставить предохранители в цепочке

Порядок регистрации решает, что увидит предохранитель. Правило простое: страховки — раньше, бизнес-логика — позже. Recoverer ставят первым, чтобы паника в любом слое ниже была поймана; Timeout и Throttle следуют сразу за ним и перед собственной бизнес-логикой. Логика такая: Throttle должен считать запросы до того, как они потратят силы на декодирование тела или проверку токена, — иначе толпа всё равно выполнит дорогую часть работы. А Timeout обязан отсчитывать дедлайн с самого начала пути, а не с середины цепочки. В боевой конфигурации предохранители занимают место между служебными слоями вроде RequestID и Logger и регистрацией маршрутов — причём служебные полезно иметь раньше: тогда даже отклонённый по 503 запрос оставит в логах строку с идентификатором, и по ней найдут инцидент.

### Соберите боевую цепочку

Разложите слои в правильном порядке: идентификатор запроса, лог, страховка от паник, предохранители — и маршруты в самом конце.

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

1. `r := chi.NewRouter()`
2. `r.Use(middleware.RequestID)`
3. `r.Use(middleware.Logger)`
4. `r.Use(middleware.Recoverer)`
5. `r.Use(middleware.Timeout(2 * time.Second))`
6. `r.Get("/report", reportHandler)`

### Таймауты go: внешний барьер http.Server

Middleware работают внутри вашего кода, но у сервера есть собственный заслон. У http.Server есть ReadTimeout, WriteTimeout и IdleTimeout: первый ограничивает время на чтение запроса — от accept до полного тела, второй — время на запись ответа, третий держит паузу между keep-alive запросами. Это внешний барьер против внутреннего: Throttle и Timeout защищают ваш бизнес-код, а серверные таймауты — само соединение, включая клиентов, которые открыли соединение и молчат, как slowloris. Настраиваются они при создании сервера, а не через middleware: srv := &http.Server{ReadTimeout: 5 * time.Second, WriteTimeout: 10 * time.Second} вместо голого http.ListenAndServe. Правильная сборка — оба уровня сразу: сервер обрезает протокольные хвосты и медленных клиентов, middleware управляет дедлайнами конкретных хендлеров. Термины вроде OOM, keep-alive и slowloris разобраны в [глоссарии](/glossary/).

### Механизмы защиты: что делает и что отвечает

| Механизм | Что делает | Код ответа |
| --- | --- | --- |
| middleware.Timeout(d) | дедлайн на каждый хендлер; по истечении отменяет контекст запроса | 504 Gateway Timeout |
| middleware.Throttle(n) | пропускает не больше n одновременных запросов, лишние — сразу | 503 Service Unavailable |
| middleware.ThrottleBacklog(limit, backlog, timeout) | сверх limit держит очередь ожидания до backlog позиций и не дольше timeout | 503 Service Unavailable |
| ReadTimeout / WriteTimeout у http.Server | внешний барьер: обрезает медленных клиентов и долгую запись на уровне соединения | соединение обрывается |

### Минимальный набор предохранителей сервиса

- Recoverer раньше всех: паника в одном хендлере не должна ронять процесс
- Timeout на весь роутер: у каждого хендлера есть срок
- Throttle или ThrottleBacklog на тяжёлые маршруты: толпа не должна давить горутинами
- ReadTimeout и WriteTimeout у http.Server: внешний барьер против медленных клиентов
- Прокидывайте r.Context() в БД и внешние API: дедлайн должен доезжать до самого медленного места

> **Спор: 504 подождать или 503 отказать быстро.** Пользователь нажал кнопку. Что полезнее: две секунды спиннера и 504 с шансом, что запрос всё же успел, — или мгновенный 503 с честным «сейчас не справляемся»? Моё мнение: для интерактивных страниц быстрый отказ честнее — ожидание перед той же ошибкой ценности не добавляет, а 503 хотя бы даёт клиенту повод повторить попытку позже. Но для фоновых вызовов, где клиент умеет ждать, дедлайн с 504 оправдан: вдруг успеет. Выбор зависит от того, кто и зачем стучится в ваш API.

### Карточки: таймауты и троттлинг

- **Что возвращает chi middleware.Timeout по истечении дедлайна?** — 504 Gateway Timeout; контекст запроса отменяется, тело ответа — Gateway Timeout.
- **Прерывает ли Timeout выполнение хендлера?** — Нет. Горутина живёт до конца; хендлер обязан следить за r.Context().Done() и прокидывать контекст в БД и внешние API.
- **Что делает middleware.Throttle(10)?** — Пропускает до 10 одновременных запросов; 11-й и дальше сразу получают 503 Service Unavailable.
- **Чем ThrottleBacklog отличается от Throttle?** — Лишние запросы не получают мгновенный 503, а ждут в очереди до лимита backlog и не дольше backlog-таймаута.
- **Зачем ReadTimeout и WriteTimeout у http.Server?** — Внешний барьер: обрезают медленных клиентов и долгую запись на уровне соединения, даже если в коде таймаутов нет.

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

- middleware.Timeout(d) даёт каждому хендлеру дедлайн и возвращает 504 Gateway Timeout.
- Хендлер не прерывают силой: за отменой следят через r.Context().Done(), иначе горутина доработает до конца.
- Throttle(n) — мгновенный отказ 503 сверх n одновременных; ThrottleBacklog добавляет очередь ожидания с таймаутом.
- Порядок цепочки: Recoverer раньше, предохранители до бизнес-логики, маршруты в конце.
- Таймауты http.Server — внешний барьер: обрезают медленных клиентов на уровне соединения.
- Без таймаутов медленная БД копит висящие горутины — память растёт до OOM.

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

Возьмите сервис из прошлых уроков и доведите предохранители до конца: повесьте middleware.Timeout(2 * time.Second) и middleware.Throttle(5), добавьте WriteTimeout при создании http.Server. Напишите хендлер, который следит за r.Context().Done() через select, и проверьте curl-ом: запрос к медленному маршруту должен возвращать 504, а двадцать параллельных — частично 503.

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

### Почему chi.Timeout возвращает именно 504, а не 503?

504 Gateway Timeout по спецификации HTTP означает, что срок ожидания истёк, — ровно то, что произошло. Код 503 зарезервирован в chi под Throttle: перегрузка и дедлайн — разные события, и мониторинг различает их по кодам ответов. Смешивать их не стоит.

### Хендлер продолжает работать после 504 — это баг?

Нет, это устройство Go: горутину нельзя прервать силой, middleware лишь отменяет контекст. Чтобы работа реально останавливалась, следите за r.Context().Done() в select и прокидывайте контекст в запросы к БД и внешним сервисам.

### Какое значение дедлайна ставить в Timeout?

Чуть больше самого медленного нормального запроса: для веб-API обычно две-пять секунд. И помните: увеличенный дедлайн не ускоряет запрос, а лишь дольше держит ресурсы — лечить стоит медленную БД, а не растить таймаут.
