Дубликаты SMS появляются, когда система не понимает, обработал ли шлюз предыдущую попытку. Приложение ждёт ответ, получает тайм-аут и повторяет запрос, хотя первое сообщение уже ушло оператору. Защитить интеграцию можно через идемпотентный ключ, очередь отправки, хранение статуса и корректную обработку DLR. В статье разберём схему, которую можно применить в интернет-магазине, сервисе записи или другой системе малого бизнеса.
Почему API или SMPP отправляет одно SMS дважды?
Самая частая причина — повтор после неопределённого результата. Приложение отправило запрос, но соединение оборвалось до получения ответа. Для бизнеса это выглядит как ошибка. На стороне SMS-шлюза запрос уже мог попасть в очередь, поэтому повторная отправка создаёт второй экземпляр сообщения.
Похожая ситуация возникает при перезапуске сервиса. Очередь хранит задачу со статусом «в работе», процесс завершается, а после запуска считает её новой и отправляет повторно. Ещё один сценарий связан с вебхуками: система получает уведомление о событии дважды и дважды создаёт задачу на отправку.
В SMPP нужно разделять несколько событий: приложение передало PDU, шлюз принял сообщение, оператор подтвердил доставку, а клиент получил DLR. Подтверждение приёма PDU не означает, что SMS уже доставлено абоненту. Состояние сообщения лучше менять по понятной схеме, а не по одному ответу соединения.
- Создано — бизнес-система сформировала уведомление.
- Поставлено в очередь — задача получила идентификатор и ждёт отправки.
- Принято шлюзом — SMPP-сессия вернула положительный ответ.
- Доставлено или отклонено — статус подтверждён через DLR либо код ошибки.
- Повтор разрешён — система решила, что повтор безопасен и действительно нужен.
Для диагностики полезно сопоставить время запроса, внутренний идентификатор, message_id от шлюза, номер получателя и итоговый статус. Если в журнале нет такой связки, выяснить причину дубля после сбоя будет сложно. Для автоматической реакции на ошибки пригодится материал о обработке ошибок SMPP.
Что такое идемпотентный ключ для SMS?
Идемпотентный ключ — уникальный идентификатор бизнес-события. Он говорит системе: «это та же самая команда, даже если запрос пришёл повторно». Например, интернет-магазин создаёт ключ из номера заказа, типа события и версии уведомления: order-1842-status-shipped-v1. При повторном запросе сервис проверяет ключ и не создаёт новую SMS-задачу.
Ключ нужно формировать до первого обращения к API или постановки сообщения в очередь. Если генерировать случайный UUID при каждой попытке, защита не сработает: каждый повтор будет выглядеть как новая команда. Один и тот же ключ должен сохраняться при тайм-ауте, повторном запуске приложения и повторной доставке вебхука.
Для разных событий нужны разные ключи. Уведомление «заказ принят» не должно совпадать с сообщением «заказ передан в доставку». При этом повтор одного и того же события сохраняет прежний ключ.
| Ситуация | Ключ | Результат |
|---|---|---|
| Повтор запроса после тайм-аута | Тот же ключ события | Новая SMS-задача не создаётся |
| Изменился статус заказа | Новый ключ статуса | Создаётся отдельное уведомление |
| Повтор вебхука от CRM | Идентификатор события CRM | Повторная команда игнорируется |
| Ручная повторная отправка оператором | Новый ключ с отметкой повтора | Отправка фиксируется как отдельное действие |
Как построить очередь SMS без повторной отправки?
Приложение, которое обслуживает заказ или запись клиента, лучше не отправляет SMS прямо внутри основного запроса. Оно записывает событие в очередь, а отдельный обработчик забирает задачу и передаёт её через API или SMPP. Такой подход позволяет пережить временный сбой связи и отдельно контролировать повторные попытки.
- Сформируйте идемпотентный ключ из идентификатора события.
- Проверьте, есть ли этот ключ в журнале отправок.
- Если ключ новый, сохраните задачу со статусом «поставлено в очередь».
- Передайте сообщение шлюзу и запишите ответ вместе с message_id.
- Получите DLR и обновите итоговый статус, не создавая новую задачу.
Запись о ключе должна появляться атомарно. Иначе два параллельных процесса одновременно проверят пустую таблицу и оба начнут отправку. На уровне хранилища помогает уникальное ограничение на поле idempotency_key. Если второй процесс встретит конфликт, он перечитает существующую запись и продолжит работу с ней.
Для каждой задачи задайте понятные границы повтора. При временной ошибке соединения повтор может быть оправдан, но перед ним система должна проверить, не появился ли уже message_id или итоговый DLR. При постоянной ошибке, например некорректном номере или содержании, повторять ту же команду бессмысленно.
Как обрабатывать повторы в SMPP-сессии?
SMPP работает через долгоживущее соединение, поэтому приложение следит не только за отправкой PDU, но и за состоянием сессии. Периодический enquire_link поддерживает соединение активным; в документации Exolve указано, что ESME нужно отправлять такой запрос каждые 15 минут независимо от наличия трафика. При разрыве сессии очередь должна продолжить работу после переподключения, но не создавать новые задачи для уже подтверждённых сообщений.
Внутренний ключ события и message_id шлюза решают разные задачи. Первый защищает бизнес-логику от повторного создания SMS. Второй помогает сопоставить отправку с ответом шлюза и DLR. Поэтому в журнале стоит хранить оба значения, а также время попытки, код ответа SMPP и итоговый статус.
| Поле журнала | Зачем оно нужно |
|---|---|
| Идемпотентный ключ | Показывает, к какому событию относится SMS |
| Внутренний ID задачи | Связывает очередь с заказом или записью клиента |
| message_id | Помогает сопоставить ответ шлюза и DLR |
| Код SMPP | Показывает результат конкретной попытки |
| Количество попыток | Позволяет найти зацикленные повторы |
| Финальный статус | Отделяет доставку от отказа и временного ожидания |
Если интеграция использует несколько обработчиков, каждому нужен общий журнал отправок. Отдельная локальная память процесса не защитит от дублей после перезапуска или при работе нескольких копий сервиса. Для проекта с небольшим объёмом сообщений достаточно начать с одной таблицы задач и уникального ключа, а затем добавить мониторинг.
Какие ошибки чаще всего приводят к дублям?
- Приложение повторяет запрос после любого тайм-аута, не проверяя результат первой попытки.
- Новый ключ создаётся при каждом повторном обращении к API.
- Система считает положительный submit_sm_resp подтверждением доставки.
- Вебхук обрабатывается без проверки идентификатора события.
- После переподключения SMPP очередь отправляет все задачи со статусом «в работе».
- Логи хранят текст SMS, но не сохраняют message_id и код ответа.
Отдельно проверьте сценарий параллельной обработки. Два воркера могут одновременно получить одну задачу, если очередь не использует блокировку или атомарную смену статуса. У задачи должен быть владелец, срок блокировки и правило возврата в очередь после сбоя процесса.
Тестировать защиту нужно не только на успешной отправке. Имитируйте тайм-аут после передачи сообщения, разрыв SMPP-сессии, повтор вебхука, перезапуск обработчика и поздний DLR. Ожидаемый результат во всех случаях — одна бизнес-команда и одна связанная запись отправки. Для отдельной проверки канала можно использовать рекомендации по тестированию доставляемости SMS.
Как внедрить идемпотентность в небольшой компании?
Начните с одного критичного сценария: код входа, подтверждение заказа или уведомление о готовности ремонта. Опишите события и допустимые статусы, затем добавьте ключ и уникальное ограничение в журнал. После этого подключите очередь и только потом настройте автоматические повторы.
На этапе интеграции заранее определите, кто отвечает за повтор: приложение, очередь или SMS-шлюз. Когда несколько компонентов повторяют отправку одновременно, система быстро теряет предсказуемость. В рабочем SMPP-шлюзе и API-интеграции полезно также вывести на мониторинг количество задач в очереди, ошибки соединения, повторные попытки и сообщения без финального статуса.
Для малого бизнеса практичная схема выглядит так: CRM или сайт создаёт событие, интеграция присваивает ему ключ, очередь передаёт SMS, шлюз возвращает технический идентификатор, а DLR закрывает задачу итоговым статусом. Такая архитектура не требует сложной автоматизации, но её нужно заранее проверить на сбоях. Площадка smpp.by ориентирована именно на задачи SMPP, API-интеграции и аналитику трафика, поэтому эти компоненты можно проектировать как единую цепочку.
3 шага, которые можно сделать на этой неделе:
- Найдите в коде все места, где SMS повторяется после тайм-аута или ошибки.
- Добавьте постоянный ключ события и уникальную проверку перед постановкой задачи в очередь.
- Соберите журнал с message_id, кодом SMPP, количеством попыток и DLR, затем проведите тест с разрывом соединения.



