---
title: "Ack, Nack и Reject: гарантии доставки"
description: "Ack, Nack и Reject в amqp091-go: ручные подтверждения, requeue, consumer_timeout и поведение при падении воркера. Manual ack golang с примерами."
course: "RabbitMQ в Go: очереди сообщений на amqp091-go"
url: "https://golangmaster.ru/courses/rabbitmq/lessons/08-ack-nack/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 20
---

# Ack, Nack и Reject: гарантии доставки

*Курс «RabbitMQ в Go: очереди сообщений на amqp091-go», урок 8 из 25. ~20 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/rabbitmq/lessons/08-ack-nack/*

Гарантии rabbitmq ack начинаются с одного факта: без ручного подтверждения брокер считает сообщение обработанным в момент отправки. Ручные ack превращают чтение из очереди в договор — обработал, отчитайся; не успел — сообщение вернётся и будет доставлено заново. Разберём d.Ack, d.Nack и d.Reject, флаг multiple, судьбу неподтверждённых сообщений при падении воркера и ядовитые циклы, в которых requeue работает против вас.

Седьмой урок оставил открытым главный вопрос потребителя: что именно подтверждает d.Ack и что случается, если подтверждения нет. Сообщение в очереди живёт в двух состояниях: ready — ждёт доставки, unacknowledged — выдано воркеру и ждёт вердикта. Пока воркер не подтвердил доставку, брокер хранит сообщение отдельно и никому его больше не отдаст; подтвердил — удаляет навсегда. Вся надёжность системы упирается в момент между этими состояниями: упал воркер с неподтверждённой доставкой, потерялась сеть, завис процесс — брокер возвращает сообщение в очередь и находит ему нового потребителя. Это и есть ручные подтверждения, manual ack: простейший механизм с парой методов и тремя-четырьмя правилами, на которых тем не менее стоят все гарантии доставки курса. Разберём методы по одному, потом проиграем падение воркера, а закончим самой дорогой ошибкой потребителя — бесконечным возвратом ядовитого сообщения.

### Manual ack golang: метод d.Ack и флаг multiple

Базовое подтверждение в manual ack golang выглядит как d.Ack(false): воркер сообщает брокеру, что доставку с этим DeliveryTag обработал, и сообщение можно удалить. Единственный аргумент multiple расширяет действие: d.Ack(true) подтверждает не только текущее, но и все доставки с меньшими тегами на этом канале, которые ещё не подтверждены. Теги растут монотонно в рамках канала, поэтому батч-подтверждение удобнее всего, когда обработки идут по порядку — на пакетных сценариях это экономит десятки сетевых рейсов. Правило канала то же, что и везде: подтверждение отправляется на том же канале, который доставил сообщение; ack чужим каналом даст ошибку неизвестного тега, и канал закроется. Двойное подтверждение одной доставки — та же история: второй ack улетает с тегом, который брокер уже удалил, канал получает PRECONDITION_FAILED и закрывается. Метод возвращает error, но не ждёт встречного кадра: ошибка подтверждения проявится либо в возврате метода при закрытом канале, либо через NotifyClose, — ещё один аргумент держать канал наблюдаемым, как в четвёртом уроке.

### Падение воркера: что станет с неподтверждённым

Проиграем аварию по шагам. Воркер получил доставку — сообщение перешло в unacknowledged. Процесс умер: от паники без recover, от деплоя, от kill -9. Брокер замечает это по закрытию канала или соединения — все неподтверждённые доставки этого потребителя возвращаются в очередь. Возвращённые сообщения встают максимально близко к началу очереди, получают флаг Redelivered=true и уходят первыми же живым потребителям. Из этого следует практика, которую дешевле принять сразу: обработчик должен быть готов к повторной доставке, а бизнес-операция — к дублю. Гарантия, которую даёт ручной ack, называется at-least-once: сообщение доставляется хотя бы один раз, но при сбоях возможны повторы, и от дубликатов защищает дедупликация по MessageId или бизнес-ключу. Противоположная крайность — autoAck=true из седьмого урока: там повторы исключены, зато падение воркера теряет всё выданное. Промежуточных гарантий у очереди нет: либо вы подтверждаете после обработки и миритесь с дублями, либо подтверждаете до и миритесь с потерями.

### Воркер с Ack и Nack: три исхода доставки

```go
package main

import (
        "context"
        "errors"
        "log"

        amqp "github.com/rabbitmq/amqp091-go"
)

var errPoison = errors.New("не удается обработать сообщение")

func process(body []byte) error {
        if len(body) == 0 {
                return errPoison
        }
        log.Println("обработано:", string(body))
        return nil
}

func main() {
        conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
        if err != nil {
                log.Fatal(err)
        }
        defer conn.Close()

        ch, err := conn.Channel()
        if err != nil {
                log.Fatal(err)
        }

        _, err = ch.QueueDeclare("tasks", true, false, false, false, nil)
        if err != nil {
                log.Fatal(err)
        }

        ctx, cancel := context.WithCancel(context.Background())
        defer cancel()

        msgs, err := ch.ConsumeWithContext(ctx, "tasks", "ack-worker",
                false, false, false, false, nil)
        if err != nil {
                log.Fatal(err)
        }

        for d := range msgs {
                if err := process(d.Body); err != nil {
                        if errors.Is(err, errPoison) {
                                d.Nack(false, false)
                                continue
                        }
                        d.Nack(false, true)
                        continue
                }
                if err := d.Ack(false); err != nil {
                        log.Println("ack failed:", err)
                }
        }
}
```

Три исхода в одном цикле: успех — Ack(false), временная ошибка — Nack(false, true) с возвратом в очередь, безнадёжное тело — Nack(false, false) без возврата. Никакого ожидания между доставкой и вердиктом брокер не требует: подтверждение — это отдельный кадр, а не ответ на доставку.

### Nack и Reject: три пути неудачной доставки

Nack и Reject отказываются от доставки, и разница между ними в двух измерениях. d.Reject(requeue bool) — протокольный минимум: отказ от одной доставки, requeue решает, возвращать сообщение в очередь или нет. d.Nack(multiple, requeue bool) — расширение RabbitMQ: то же самое, но с возможностью махнуть сразу на все неподтверждённые доставки с меньшими тегами; для одиночных доставок Nack(false, requeue) эквивалентен Reject(requeue), и в коде их путать не вредно, хотя команда обычно выбирает один глагол. Ветка requeue=true возвращает сообщение, и оно немедленно уходит потребителям снова — с Redelivered=true. Ветка requeue=false удаляет сообщение, если у очереди нет dead letter exchange, и перекладывает его в DLQ, если настроен; разбор DLX и лимитов повторов — в шестнадцатом уроке, здесь достаточно правила: requeue=false без DLX — это стирание сообщения, и оно должно быть осознанным. Третий путь, о котором часто забывают, — отсутствие всякого пути: воркер молча ушёл без ack и nack, и сообщение вернётся само по закрытию канала. Это легальный вариант для обрывов и падений, но не для управления потоком.

### Ack, Nack и Reject рядом

| Вызов | Смысл | Когда применять |
| --- | --- | --- |
| d.Ack(false) | подтвердить одну доставку | обработка прошла успешно |
| d.Ack(true) | подтвердить все доставки до текущей | пакетная обработка с известным порядком |
| d.Nack(false, true) | вернуть одну доставку в очередь | временная ошибка: сеть, недоступная зависимость |
| d.Nack(false, false) | не возвращать: удалить или в DLQ | сообщение не станет валидным от повтора |
| d.Reject(requeue) | отказ от одной доставки | то же, что Nack на одну, выбор — вопрос вкуса команды |

> **Ядовитое сообщение и бесконечный requeue.** Кейс: воркер читал JSON, на ошибке разбора делал Nack(false, true) — вернуть в очередь. Одно битое сообщение от заказчика с выгрузкой разрушило вечер: возврат ставит его в начало очереди, воркер тут же получает его снова, снова не может разобрать, снова возвращает. Процесс крутится на полном ядре, полезные сообщения стоят за ядом, лог растёт мегабайтами в минуту. Лечение двухслойное: разбор ошибок на классы — невалидное сообщение уходит с requeue=false в DLQ, а временные сбои возвращаются; иногда добавляют счётчик повторов в заголовках, чтобы третий раз стал последним. Урок шестнадцатый собирает это в готовый паттерн retry с dead letter.

### Consumer timeout: когда ack приходит слишком поздно

У ручных подтверждений есть скрытый лимит: брокер ждёт ack на доставку не вечно. Начиная с релизов 3.8.15 у RabbitMQ действует consumer_timeout — по умолчанию тридцать минут на одну доставку; если воркер не подтвердил сообщение за это время, брокер закрывает канал с ошибкой PRECONDITION_FAILED про acknowledgement timeout, а неподтверждённое возвращается в очередь. Ловится это на длинных задачах: обработка видео, тяжёлые импорты, пауза в отладчике на живой очереди. Способы честные: дробить задачу на шаги с промежуточными подтверждениями, выносить прогресс в отдельное хранилище и подтверждать исходное сообщение сразу, либо поднимать consumer_timeout в конфигурации брокера — что осознанно, а не по привычке. Симптом узнаваем: воркер работает, раз в полчаса канал закрывается, сообщения возвращаются — и цикл повторяется. Теперь вы знаете, что это не гонка и не сеть, а лимит протокола, который Broker считает честным для обычных потребителей.

### Батч-подтверждение через multiple

```go
var last amqp.Delivery
pending := 0

for d := range msgs {
        if err := process(d.Body); err != nil {
                d.Nack(false, true)
                continue
        }
        last = d
        pending++
        if pending == 10 {
                if err := last.Ack(true); err != nil {
                        log.Println("batch ack failed:", err)
                }
                pending = 0
        }
}
```

Каждый десятый вызов подтверждает сразу партию: Ack(true) на последней доставке закрывает все неподтверждённые с меньшими тегами. Экономия сетевых рейсов ощутимая, цена тоже: сбой между обработкой и батчем вернёт в очередь до девяти уже сделанных задач — их повторно сделает другой воркер, и это должно быть нормально для вашей бизнес-логики.

### Честная цена гарантий at-least-once

Соберём всю цепочку гарантий, которые к этому уроку уже собраны. Продюсер отправил сообщение,mandatory возвращает немаршрутизируемое — шестой урок; очередь хранит непрочитанное; ручной ack возвращает необработанное — этот урок. Итог называется at-least-once: система гарантирует доставку хотя бы один раз, но не ровно один. Дубли возникают в двух местах: продюсер повторил отправку после обрыва, не узнав, дошло ли; брокер вернул сообщение после сбоя воркера, который уже успел записать результат. Поэтому идемпотентность потребителя — не украшение, а условие работы схемы: повторная обработка заказа не должна создавать второй заказ. Практика: ключ дедупликации в заголовках или MessageId, проверка в обработчике, уникальные ограничения в базе как последний рубеж. Ровно один раз из коробки не обещает ни один брокер сообщений, и любые «exactly-once» маркетинговые формулировки читайте как at-least-once плюс дедупликация на вашей стороне.

> **Код урока — локально, интерактивы — на странице.** Подтверждения — протокольный обмен с брокером, в браузерной песочнице ему взяться неоткуда: там работают только fmt, strings, strconv, math, sort, errors и time, без сети. Воркер этого урока запускается локально против Docker-брокера из второго урока; чтобы увидеть возврат, добавьте в очередь битое сообщение продюсером из шестого урока и понаблюдайте Redelivered. Интерактивы ниже проверят аргументы методов и судьбу неподтверждённой доставки.

### Правила подтверждений

- Ack после обработки, не до: подтверждение раньше результата — это autoAck под другим именем.
- К обработке повторов готовятся заранее: дедупликация по MessageId или бизнес-ключу.
- Ошибки делят на классы: временные — Nack с requeue, невалидные — requeue=false и DLQ.
- Бесконечный requeue яда — инвалидация воркера; счётчик повторов или DLX ставят до инцидента.
- Ack только на канале-доставщике; чужой канал или двойной ack закрывают канал PRECONDITION_FAILED.
- Длинные задачи держат в уме consumer_timeout: тридцать минут на доставку по умолчанию.

Маршрут дальше: [девятый урок](/courses/rabbitmq/lessons/09-durable-persistent/) закрывает вторую половину аварий — падение самого брокера: durable-очередь и DeliveryMode решают, что переживёт рестарт. Возврат неудачных сообщений в систему, а не в никуда, — [шестнадцатый урок про dead letter](/courses/rabbitmq/lessons/16-obrabotka-oshibok/), остановка воркера с дожимом задач — [двадцать первый про graceful shutdown](/courses/rabbitmq/lessons/21-graceful-rabbitmq/). Типовые способы потерять сообщения собраны в [двадцать третьем уроке про антипаттерны](/courses/rabbitmq/lessons/23-antipatterny-ocheredey/), а тестируемость обработчика Delivery — в [двадцать четвёртом](/courses/rabbitmq/lessons/24-testy-rabbitmq/). Похожую механику подтверждений вы могли видеть в [потоках Redis](/courses/go-redis/lessons/15-potoki-streams/) — XACK решает ту же задачу в другой терминологии. Подписка и цикл воркера остались в [седьмом уроке](/courses/rabbitmq/lessons/07-potreblenie-soobschenij/), карта курса — на [странице курса](/courses/rabbitmq/).

### Проверка: падение воркера

**Вопрос:** Воркер с autoAck=false упал, не подтвердив две доставки. Что станет с этими сообщениями?

- Останутся в состоянии unacknowledged навсегда
- ✅ Вернутся в очередь с Redelivered=true и уйдут живому потребителю
- Будут удалены, так как обработка не завершилась
- Уйдут в dead letter queue автоматически

Закрытие канала или соединения возвращает все неподтверждённые доставки в очередь, они встают ближе к началу и помечаются Redelivered. DLQ тут ни при чём: она подключается через dead letter exchange и работает по своим правилам из шестнадцатого урока.

### Проверка: multiple

**Вопрос:** Воркер вызвал d.Ack(true) на доставке с тегом 10. Что подтвердится?

- Только доставка с тегом 10
- ✅ Все доставки с тегами от 1 до 10, ещё не подтверждённые на этом канале
- Все доставки очереди, включая чужие каналы
- Ошибка: multiple доступен только у Nack

Флаг multiple подтверждает все доставки с меньшими тегами на том же канале вместе с текущей. Чужие каналы не затрагиваются: подтверждение привязано к каналу, который эти доставки получил.

### Что напечатает воркер?

```go
// в очереди tasks ровно одно сообщение, других нет
first := true
for d := range msgs {
        fmt.Println(d.Redelivered)
        if first {
                first = false
                d.Nack(false, true) // вернуть в очередь
                continue
        }
        d.Ack(false)
}
```

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

- ✅ false, затем true — возврат немедленно приедет обратно
- false, затем false — флаг Redelivered не ставится
- true, затем true — возврат помечает сообщение дважды
- ничего: Nack закрывает канал

Первая доставка помечена как новая — печатается false. Nack(false, true) возвращает сообщение, оно встаёт в начало очереди и немедленно доставляется снова уже с Redelivered=true. На одиночной очереди возврат почти мгновенный: именно так выглядят яд-циклы из warn-врезки.

### Соберите цикл с вердиктами

Допишите проверку повтора, возврат при ошибке и подтверждение при успехе.

```go
for d := range msgs {
        if d.___ {
                log.Println("повторная доставка")
        }
        if err := process(d.Body); err != nil {
                d.___(false, true)
                continue
        }
        d.___(false)
}
```

Подсказка: Флаг повтора — поле Redelivered, возврат в очередь — Nack(false, true), подтверждение — Ack(false).

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

### Соберите обработку доставки по шагам

Расставьте строки в порядке выполнения: соединение, канал, подписка, чтение, обработка, подтверждение.

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

1. `conn, err := amqp.Dial(url)`
2. `ch, err := conn.Channel()`
3. `msgs, err := ch.ConsumeWithContext(ctx, "tasks", "", false, false, false, false, nil)`
4. `d := <-msgs`
5. `err = process(d.Body)`
6. `d.Ack(false)`

### Карточки: Ack, Nack, Reject

- **d.Ack(multiple)** — Подтверждение доставки: false — одна, true — все неподтверждённые с меньшими тегами на канале. Отправляется на канале-доставщике.
- **d.Nack(multiple, requeue)** — Отказ от доставки, расширение RabbitMQ: multiple — махнуть партией, requeue — вернуть в очередь или удалить/передать в DLQ.
- **d.Reject(requeue)** — Протокольный отказ от одной доставки. На одиночных сообщениях эквивалентен Nack(false, requeue).
- **Redelivered** — Флаг повторной доставки: сообщение уже отдавалось и вернулось без подтверждения. Готовность к дублю — условие at-least-once.
- **consumer_timeout** — Лимит ожидания ack: по умолчанию 30 минут, затем канал закрывается PRECONDITION_FAILED, а доставки возвращаются в очередь.
- **at-least-once** — Гарантия ручных ack: сообщение доставляется хотя бы один раз; дубли возможны и гасятся дедупликацией потребителя.

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

- Ручной ack — договор с брокером: обработал и подтвердил, либо вернёт доставку при закрытии канала.
- Ack(true) подтверждает партию по тегам одного канала; двойной ack или чужой канал дают PRECONDITION_FAILED.
- Падение воркера возвращает неподтверждённые доставки в очередь с Redelivered=true — обработчик обязан переживать повторы.
- Nack и Reject отказываются от доставки: requeue=true возвращает, requeue=false удаляет или уводит в DLQ.
- Ядовитое сообщение с бесконечным requeue парализует воркер — классы ошибок и DLX ставят заранее.
- consumer_timeout ограничивает ожидание ack тридцатью минутами по умолчанию — длинные задачи дробят.
- Итог гарантий — at-least-once: дубли возможны, дедупликация на стороне потребителя обязательна.

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

Запустите воркер из урока и опубликуйте пять сообщений, одно из которых — пустая строка; убедитесь, что яд ушёл с Nack(false, false), а остальные подтвердились. Затем прервите воркер по Ctrl+C посреди обработки и посмотрите в management UI, как сообщения возвращаются и получают Redelivered=true у следующего запуска. В конце соберите батч-подтверждение на десять доставок и оцените по логам, сколько сообщений вернётся при падении посреди партии.

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

### Чем Nack отличается от Reject, если на одной доставке они равны?

Только флагом multiple: Nack умеет отказываться от всех неподтверждённых доставок с меньшими тегами, Reject работает строго с одной. Команды обычно выбирают один глагол и держат его во всём коде, чтобы в ревью не гадать, что имелось в виду.

### Что делать с задачами, которые обрабатываются дольше consumer_timeout?

Дробить на шаги с промежуточными подтверждениями или выносить прогресс во внешнее хранилище, подтверждая исходную доставку сразу после приёмки. Третьим вариантом остаётся поднять consumer_timeout в конфигурации брокера — но это осознанное решение для конкретных очередей, а не настройка по умолчанию на весь кластер.

### Гарантирует ли ручной ack, что сообщение обработано ровно один раз?

Нет: ack даёт at-least-once — хотя бы один раз, с возможными повторами при сбоях. Ровно один раз собирают поверх очереди: дедупликация по ключу в обработчике, уникальные ограничения в базе, идемпотентные операции. Очередь обеспечивает возврат незавершённого, а не отсутствие дублей.
