Перейти к содержанию

Idempotency keys в n8n: как не создавать дубли при повторах

Обновлено: 2026-05-29

Открыть мой план

Короткий ответ

Idempotency key — это уникальный ключ операции, который позволяет безопасно повторить событие и не выполнить бизнес-действие дважды. В n8n он нужен для webhook, платежей, CRM, очередей, AI tools и любых workflow, где возможны retry, replay или повторная доставка. Правильная схема хранит ключ, статус операции, результат, время создания и причину ошибки. Без idempotency workflow может создать два лида, два счёта, два тикета или два списания из-за одного и того же события.

Почему дубли появляются даже в “правильном” workflow

Дубли не всегда означают ошибку кода. Большинство внешних систем специально повторяют webhook, если не получили быстрый успешный ответ. Пользователь может дважды нажать кнопку. HTTP Request node может быть настроен на retry. Оператор может вручную перезапустить failed execution. Queue worker может упасть после записи в CRM, но до записи внутреннего статуса. AI Agent может повторить tool call, если не получил структурированный ответ. Во всех этих случаях одно бизнес-событие проходит через workflow больше одного раза.

Идемпотентность отвечает на вопрос: “Выполняли ли мы уже именно эту операцию?” Не “видели ли похожий email”, не “есть ли такой телефон в CRM”, а была ли уже обработана конкретная команда. Это особенно важно для денег, CRM, рассылок, складских остатков и действий AI-агента.

Из чего строить idempotency key

Хороший ключ должен быть стабильным, уникальным для операции и не зависеть от случайных полей. Если источник даёт event ID — используйте его. Если event ID нет, соберите ключ из типа операции и внешнего идентификатора. Например: payment.succeeded:yookassa:2f5a..., lead.created:form:submission_123, ticket.reply:zendesk:comment_987, ai_tool.crm_update:request_abc.

Не используйте как ключ только email, телефон, имя клиента или сумму платежа. Эти поля не описывают операцию. Один клиент может оставить две заявки, два платежа могут быть на одинаковую сумму, email может измениться, а телефон может принадлежать нескольким лидам. Если приходится собирать ключ из нескольких полей, добавьте нормализацию и фиксированный порядок: source:event_type:external_id:version.

Где хранить ключи

Для production лучше хранить ключи не в памяти n8n и не в Google Sheets, а в базе: Postgres, Redis с persistence или отдельная таблица в вашей системе. Минимальная таблица:

CREATE TABLE n8n_idempotency_keys (
  idempotency_key text PRIMARY KEY,
  status text NOT NULL,
  correlation_id text,
  workflow_name text,
  external_event_id text,
  result jsonb,
  error_code text,
  created_at timestamptz DEFAULT now(),
  updated_at timestamptz DEFAULT now(),
  expires_at timestamptz
);

Статусы должны быть понятными: processing, succeeded, failed_retryable, failed_final, quarantined. Если второй запуск видит succeeded, он возвращает сохранённый результат или завершает обработку без повторного действия. Если видит processing, он либо ждёт, либо отдаёт 202 accepted, либо пишет в retry queue. Если видит failed_retryable, можно разрешить controlled replay.

Алгоритм в n8n

Типовой workflow: Trigger → Normalize payload → Build idempotency key → Try insert key → Branch by result → Business action → Save result. Ключевой момент — операция “зарезервировать ключ” должна быть атомарной. В Postgres используйте INSERT ... ON CONFLICT DO NOTHING или transaction. Если сначала проверить SELECT, а потом отдельно вставить, два параллельных запуска могут пройти проверку одновременно.

Пример SQL для reservation:

INSERT INTO n8n_idempotency_keys
  (idempotency_key, status, correlation_id, workflow_name, external_event_id)
VALUES
  ($1, 'processing', $2, 'lead_to_crm', $3)
ON CONFLICT (idempotency_key) DO NOTHING
RETURNING idempotency_key, status;

Если запрос вернул строку — это первый запуск. Если не вернул — ключ уже существует, нужно прочитать текущий статус и решить, что делать. После успешного действия обновите строку:

UPDATE n8n_idempotency_keys
SET status = 'succeeded', result = $2::jsonb, updated_at = now()
WHERE idempotency_key = $1;

Такой подход защищает даже при параллельных webhook и queue workers.

Идемпотентность для webhook

Для webhook часто лучше быстро принять событие и вернуть ответ, а тяжёлую работу выполнить асинхронно. Но если workflow делает бизнес-действие сразу, ключ должен ставиться до CRM/API/email. Если внешний сервис повторит доставку через минуту, второй запуск увидит succeeded или processing.

Не путайте HTTP-успех и бизнес-успех. Webhook может вернуть 200, чтобы остановить повторную доставку, но внутри поставить событие в quarantine, если контракт не прошёл validation. В этом случае idempotency row нужен всё равно: иначе при ручном replay вы не поймёте, что это то же событие.

Идемпотентность для AI tools

AI Agent может вызвать tool несколько раз: из-за плохого prompt, таймаута, retry, invalid JSON или неясного результата. Если tool меняет CRM, отправляет email, создаёт счёт или обновляет статус, входной контракт tool должен требовать idempotency key. Не позволяйте агенту генерировать ключ свободным текстом. Лучше построить его из upstream request ID и названия операции.

Ответ tool тоже должен быть идемпотентным: если операция уже выполнена, верните status: already_succeeded, result_id и тот же correlation_id. Для агента это нормальный успех, а не ошибка.

Replay без дублей

Replay нужен после временной ошибки API, сбоя worker, исправления mapping или восстановления сервиса. Но replay должен выбирать только безопасные события: failed_retryable, quarantined_after_fix, иногда processing с истёкшим timeout. Никогда не запускайте массовый replay без фильтра по event type, времени, статусу и dry-run.

Перед replay сделайте отчёт: сколько событий, какие ключи, какие внешние системы будут затронуты, есть ли уже успешные результаты. После replay сравните количество входных событий и количество бизнес-изменений. Если ключи построены правильно, повторный replay не должен создавать новые объекты.

Типовые ошибки

Ошибка номер один — ставить idempotency после бизнес-действия. Если CRM уже создала лид, а запись ключа упала, повтор создаст дубль. Вторая ошибка — хранить ключ только в execution data: после pruning или миграции вы потеряете историю. Третья ошибка — считать “найти похожий лид по телефону” полноценной идемпотентностью. Это deduplication, а не защита операции. Четвёртая ошибка — не отличать retryable failure от validation failure. Пятая — не иметь TTL-стратегии: одни ключи можно хранить 30 дней, платёжные и юридически значимые события — намного дольше.

Production checklist

Проверьте: ключ строится до первого внешнего действия; ключ атомарно резервируется; есть статусы processing/succeeded/failed; повтор успешного события не меняет внешние системы; replay читает только разрешённые статусы; результат сохраняется; errors нормализованы; TTL выбран по бизнес-риску; ключ не содержит лишние PII; alert срабатывает на много processing старше SLA; ручной replay имеет dry-run и лимиты.

FAQ

Чем idempotency отличается от deduplication?

Idempotency защищает выполнение одной операции при повторах. Deduplication ищет похожие бизнес-объекты, например похожие лиды.

Можно ли использовать execution ID n8n как idempotency key?

Обычно нет. Execution ID меняется при повторном запуске, а ключ должен быть одинаковым для одного бизнес-события.

Что делать, если внешний сервис не даёт event ID?

Соберите ключ из source, event_type, стабильного external_id и версии. Если стабильного external_id нет, добавьте upstream request ID на своей стороне.

Нужно ли хранить успешный результат?

Да, чтобы повтор мог вернуть тот же результат без повторного бизнес-действия.

Какой TTL ставить для ключей?

Зависит от риска: для лидов часто 30–90 дней, для платежей и юридически важных операций срок должен соответствовать политике хранения и аудита.