Идемпотентность простыми словами: методы HTTP и ключ идемпотентности

Идемпотентность значит, что повтор операции не меняет результат: второй запрос не создаст второй заказ и не спишет деньги дважды.

Разбираем, какие методы HTTP идемпотентны по RFC 9110, как устроен ключ идемпотентности у ЮKassa и Yandex Cloud, и запускаем рабочий сервер на Python с защитой от дублей.

Статью написал:
Ваня Буявец, продюсер, основатель Checkroi
Ваня Буявец
Основатель Checkroi, продюсер, эксперт в выборе онлайн-курсов
Все 2983 статьи автора Подписаться на Телеграм-канал
Одобрено экспертом:
Наташа Буявец, основатель Checkroi, эксперт по онлайн-курсам
Наташа Буявец
Основательница Checkroi, продюсер Youtube-каналов, эксперт по онлайн-курсам
Все 3637 экспертных мнений Подписаться на Телеграм-канал
Обложка: Идемпотентность простыми словами: методы HTTP и ключ идемпотентности

Идемпотентность — это свойство операции, при котором повтор не меняет результат: выполнили её один раз или десять, состояние системы одинаковое. Для API это значит, что повторный запрос не создаст второй заказ и не спишет деньги дважды.

Из методов HTTP идемпотентны GET, HEAD, OPTIONS, TRACE, PUT и DELETE, а POST и PATCH нет. Чтобы безопасно повторять POST, сервер принимает ключ идемпотентности: уникальную строку в заголовке, по которой он узнаёт повтор. Ниже: какие методы идемпотентны по стандарту, как устроен ключ в платёжных и облачных API, рабочий пример на Python и ошибки, из-за которых защита от дублей не срабатывает.

CheckroiCheckroiПодборка курсов по API177 курсов • 37 школСравните цены, школы, программу и найдите выгодные предложения по обучениюСравнить→

Что такое идемпотентность простыми словами

Рой спокойно нажимает кнопку вызова лифта несколько раз, а лифт едет один

Слово пришло из математики. Операция идемпотентна, если применить её к результату ещё раз и ничего не изменится: модуль числа от модуля даёт то же число, объединение множества с самим собой даёт то же множество.

Бытовой пример: кнопка вызова лифта. Нажали один раз, лифт едет. Нажали ещё пять раз от нетерпения, лифт всё равно приедет один. А вот кнопка «добавить в корзину» без защиты от повторов при пяти нажатиях положит пять товаров.

То же самое в коде на Python:

x = -7
print(abs(x), abs(abs(x)))

s = {"python", "sql"}
print(s | s == s)

print(max(5, max(5, 3)) == max(5, 3))

counter = 0
def add_one():
    global counter
    counter += 1
    return counter

add_one(); add_one()
print(counter)
7 7
True
True
2

Первые три операции идемпотентны. Последняя нет: каждый вызов add_one() меняет счётчик, и после двух вызовов в нём 2, а не 1.

Важная оговорка. Идемпотентность не означает, что ответ всегда одинаковый. Она про эффект: что изменилось на сервере после запроса. Ответ на повтор может отличаться, и это нормально.

Идемпотентные методы HTTP

Какие методы HTTP идемпотентны, определяет стандарт RFC 9110, раздел 9.2.2. Метод считается идемпотентным, если несколько одинаковых запросов дают на сервере тот же задуманный эффект, что и один. Из методов этого стандарта такими названы PUT, DELETE и все безопасные методы.

Безопасными называют методы, которые по смыслу только читают данные: GET, HEAD, OPTIONS и TRACE. Любой безопасный метод идемпотентен, но не наоборот.

Метод Что делает Безопасный Идемпотентный
GET получить ресурс да да
HEAD получить только заголовки да да
OPTIONS узнать, что разрешено да да
PUT заменить ресурс целиком нет да
DELETE удалить ресурс нет да
POST создать ресурс или запустить действие нет нет
PATCH изменить часть ресурса нет нет

PATCH описан в отдельном стандарте, RFC 5789. Там сказано, что PATCH не является ни безопасным, ни идемпотентным, но запрос можно составить так, чтобы он стал идемпотентным. Например, «поставь статус paid» идемпотентен, а «прибавь к балансу 500» нет.

Почему DELETE идемпотентен, если второй раз отвечает 404

Первый DELETE /orders/42 удаляет заказ и отвечает 204. Второй находит пустоту и отвечает 404. Ответы разные, но эффект один: заказа 42 нет.

Стандарт прямо допускает такое: повтор даёт тот же задуманный эффект, «even if the original request succeeded, though the response might differ». Поэтому 404 на повторный DELETE клиенту стоит считать успехом, а не ошибкой.

Канал основателя Checkroi Вани Буявца3 700 человек читают мой Телеграм-канал про нейросетиСобрал промпты для Claude Code и ChatGPT, разборы ИИ-инструментов и лайфхаки по продвижению бизнеса в одном местеПрисоединиться

Зачем это нужно: повторы при сбоях сети

Самая неприятная ситуация в сетевом обмене выглядит так. Клиент отправил запрос, сервер его выполнил, а ответ потерялся: оборвалось соединение, сработал таймаут, мобильный интернет пропал в лифте. Клиент не знает, прошла ли операция.

Дальше у него два плохих варианта:

  • не повторять и, возможно, потерять заказ или платёж;
  • повторить и, возможно, создать дубль.

Идемпотентность убирает этот выбор: повтор становится безопасным. Поэтому RFC 9110 не рекомендует клиенту (SHOULD NOT) автоматически повторять неидемпотентный запрос, если нет способа понять, что повтор ничего не испортит. Прокси повторять такие запросы запрещено (MUST NOT).

В документации Yandex Cloud разобран пример с гонкой. Клиент остановил виртуальную машину, но ответа не получил. Тем временем другой пользователь её снова запустил. Клиент восстанавливает связь, повторяет запрос без ключа, и машина, нужная второму пользователю, останавливается ещё раз.

Бывает, что повтор провоцирует сам сервер. ЮKassa предупреждает, что код 500 не означает ни успеха, ни провала операции. Результат нужно сначала узнать и только потом решать, что делать с платежом.

Ключ идемпотентности: как он работает

Довольный Рой держит большой ключ с биркой, по которому сервер узнаёт повторный запрос

POST по определению неидемпотентен, а создавать заказы и принимать платежи на сайте как-то надо. Для этого придумали ключ идемпотентности. Клиент генерирует уникальную строку для каждой операции и передаёт её в заголовке. Сервер запоминает ключ вместе с результатом.

Если пришёл повтор с тем же ключом, сервер не выполняет операцию заново, а возвращает сохранённый ответ. Так идемпотентным становится любой метод, включая POST. Механизм не зависит от стиля API: ключ одинаково работает и в REST, и в командах вроде запуска виртуальной машины в Yandex Cloud.

POST /v3/payments HTTP/1.1
Host: api.example.ru
Content-Type: application/json
Idempotency-Key: 8e03978e-40d5-43e8-bc93-6894a57f9324

{"amount": 4900, "currency": "RUB"}

Единого стандарта на этот заголовок пока нет. Его описывает черновик IETF «The Idempotency-Key HTTP Header Field»: седьмая версия вышла 15 октября 2025 года, срок черновика истёк 18 апреля 2026 года, и RFC он так и не стал. Поэтому имя заголовка, формат ключа и срок хранения у каждого API свои.

API Заголовок Формат ключа Сколько хранится
ЮKassa Idempotence-Key любое уникальное значение до 64 символов, советуют UUID версии 4 24 часа
Yandex Cloud Idempotency-Key UUID, рекомендуется версии 4 в документации не указано
Stripe Idempotency-Key до 255 символов, советуют UUID версии 4 в API v1 ключ могут удалить автоматически, когда ему исполнится 24 часа
черновик IETF Idempotency-Key рекомендует UUID или похожую случайную строку политику срока задаёт сам сервис

Обратите внимание на разницу в одну букву: у ЮKassa заголовок Idempotence-Key, а не Idempotency-Key. Перепутать легко, и ключ просто не дойдёт до проверки. Stripe попал в таблицу как образец подробной документации: в списке стран, где он принимает платежи, России нет.

Что сервер делает с ключом

  1. Получает запрос и ищет ключ в хранилище.
  2. Ключа нет: сохраняет его вместе с отпечатком тела запроса (хешем) и выполняет операцию.
  3. После выполнения записывает рядом код и тело ответа.
  4. Ключ есть и ответ готов: отдаёт сохранённый ответ, ничего не выполняя.
  5. Ключ есть, а тело запроса другое: отказывает с кодом 422.
  6. Ключ есть, но первый запрос ещё выполняется: отказывает с кодом 409.

Коды 422 и 409 для этих случаев предлагает тот же документ IETF. Если операция требует ключа, а его не прислали, он рекомендует ответить 400 со ссылкой на документацию.

Эти правила стоит описать в спецификации API заранее, до первой строки кода. Обычно это работа системного аналитика: он договаривается с бэкендом и интеграторами, какой заголовок, сколько живёт ключ и что возвращать на повтор.

Как реализовать идемпотентность: пример на Python

Два одинаковых запроса превращаются в одну строку в таблице заказов

Соберём минимальный сервер заказов на стандартной библиотеке Python, без фреймворков. Ключи и заказы храним в SQLite, ответы отдаём в JSON. Код проверен на Python 3.14.7.

import hashlib
import json
import sqlite3
import threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

db = sqlite3.connect("orders.db", check_same_thread=False)
db.execute("CREATE TABLE IF NOT EXISTS orders (id INTEGER PRIMARY KEY, item TEXT, amount INTEGER)")
db.execute("""CREATE TABLE IF NOT EXISTS idempotency_keys (
    key TEXT PRIMARY KEY,
    body_hash TEXT NOT NULL,
    status INTEGER,
    response TEXT
)""")
db.commit()
lock = threading.Lock()


class Handler(BaseHTTPRequestHandler):
    def send(self, status, payload):
        data = json.dumps(payload, ensure_ascii=False).encode()
        self.send_response(status)
        self.send_header("Content-Type", "application/json")
        self.end_headers()
        self.wfile.write(data)

    def do_POST(self):
        raw = self.rfile.read(int(self.headers["Content-Length"]))
        key = self.headers.get("Idempotency-Key")
        if not key:
            self.send(400, {"error": "нужен заголовок Idempotency-Key"})
            return
        body_hash = hashlib.sha256(raw).hexdigest()

        with lock:
            row = db.execute(
                "SELECT body_hash, status, response FROM idempotency_keys WHERE key = ?",
                (key,),
            ).fetchone()
            if row:
                saved_hash, status, response = row
                if saved_hash != body_hash:
                    self.send(422, {"error": "ключ уже использован с другим телом запроса"})
                elif status is None:
                    self.send(409, {"error": "запрос с этим ключом ещё выполняется"})
                else:
                    self.send(status, json.loads(response))
                return
            db.execute(
                "INSERT INTO idempotency_keys (key, body_hash) VALUES (?, ?)",
                (key, body_hash),
            )
            db.commit()

        order = json.loads(raw)
        cur = db.execute(
            "INSERT INTO orders (item, amount) VALUES (?, ?)",
            (order["item"], order["amount"]),
        )
        result = {"order_id": cur.lastrowid, "item": order["item"]}
        db.execute(
            "UPDATE idempotency_keys SET status = ?, response = ? WHERE key = ?",
            (201, json.dumps(result, ensure_ascii=False), key),
        )
        db.commit()
        self.send(201, result)

    def log_message(self, *args):
        pass


ThreadingHTTPServer(("127.0.0.1", 8000), Handler).serve_forever()

Запускаем сервер и шлём запросы через curl. Сначала дважды один и тот же заказ с одним ключом, потом тот же ключ с другой суммой, потом запрос без ключа:

K=8e03978e-40d5-43e8-bc93-6894a57f9324
curl -s -X POST http://127.0.0.1:8000/orders -H "Idempotency-Key: $K" -d '{"item": "курс по SQL", "amount": 4900}'
curl -s -X POST http://127.0.0.1:8000/orders -H "Idempotency-Key: $K" -d '{"item": "курс по SQL", "amount": 4900}'
curl -s -X POST http://127.0.0.1:8000/orders -H "Idempotency-Key: $K" -d '{"item": "курс по SQL", "amount": 9800}'
curl -s -X POST http://127.0.0.1:8000/orders -d '{"item": "курс по SQL", "amount": 4900}'
{"order_id": 1, "item": "курс по SQL"} HTTP 201
{"order_id": 1, "item": "курс по SQL"} HTTP 201
{"error": "ключ уже использован с другим телом запроса"} HTTP 422
{"error": "нужен заголовок Idempotency-Key"} HTTP 400

Код ответа дописан в вывод флагом -w ' HTTP %{http_code}\n'. Повтор вернул тот же order_id: 1, и в таблице orders после четырёх запросов ровно одна строка: 1|курс по SQL|4900.

Ключ записывается в хранилище до выполнения операции, под блокировкой. Именно это закрывает гонку, когда два одинаковых запроса приходят одновременно: второй увидит запись без готового ответа и получит 409. В продакшене вместо threading.Lock обычно опираются на уникальный индекс в базе или на блокировку в Redis, потому что сервер работает в нескольких процессах.

Идемпотентность без ключа: на уровне SQL

Иногда ключ не нужен, достаточно правильно сформулировать операцию. Сравните прибавку и присвоение в SQL:

CREATE TABLE balance (user_id INTEGER PRIMARY KEY, amount INTEGER);
INSERT INTO balance VALUES (1, 1000);

-- не идемпотентно: каждый повтор добавляет ещё 500
UPDATE balance SET amount = amount + 500 WHERE user_id = 1;
UPDATE balance SET amount = amount + 500 WHERE user_id = 1;
SELECT 'прибавка', amount FROM balance;

-- идемпотентно: сколько ни повторяй, итог один
UPDATE balance SET amount = 1500 WHERE user_id = 1;
UPDATE balance SET amount = 1500 WHERE user_id = 1;
SELECT 'присвоение', amount FROM balance;

CREATE TABLE payments (external_id TEXT PRIMARY KEY, amount INTEGER);
INSERT INTO payments VALUES ('pay-2026-10-18-001', 4900) ON CONFLICT (external_id) DO NOTHING;
INSERT INTO payments VALUES ('pay-2026-10-18-001', 4900) ON CONFLICT (external_id) DO NOTHING;
SELECT 'платежей', COUNT(*) FROM payments;
прибавка|2000
присвоение|1500
платежей|1

Проверено в SQLite 3.51. Два повтора прибавки дали 2000 вместо 1500. Присвоение и вставка с ON CONFLICT DO NOTHING по внешнему идентификатору платежа выдержали повтор без последствий. Тот же синтаксис ON CONFLICT есть в PostgreSQL.

Частые ошибки

Ключ идемпотентности легко внедрить так, что он ничего не защищает. Вот что встречается чаще всего.

  • Новый ключ на каждый повтор. Если клиент генерирует UUID внутри функции отправки, то каждая попытка получает свой ключ, и сервер видит три разные операции. Ключ создают один раз на операцию (например, при оформлении заказа) и переиспользуют во всех повторах.
  • Ключ сохраняют после операции. Сервер сначала списал деньги, потом записал ключ, и между этими шагами процесс упал. Повтор пройдёт как новый. Надёжнее записывать ключ и выполнять операцию в одной транзакции или ставить ключ заранее со статусом «выполняется», как в примере выше.
  • Не сверяют тело запроса. Без отпечатка тела повтор с тем же ключом, но другой суммой, вернёт старый ответ, и клиент решит, что новый платёж прошёл. Правильный ответ здесь — ошибка, как 422 в примере выше.
  • PUT, который на деле прибавляет. Метод называется PUT, а внутри amount = amount + 500. А клиент вправе повторить PUT сам, без участия человека: стандарт HTTP приводит именно этот пример. Каждый такой повтор увеличивает баланс.
  • Забывают про срок хранения. ЮKassa хранит ключ 24 часа, после этого повтор обработают как новый запрос. Если очередь повторов на вашей стороне живёт дольше, дубли вернутся.
  • Чувствительные данные в ключе. Stripe советует не брать в качестве ключа email и другие персональные данные. Случайный UUID и надёжнее, и безопаснее.
Ваня БуявецКанал основателя Checkroi Вани БуявцаЗабирайте промпты и обучение по нейросетям в моём Телеграм-каналеБольше 3 700 человек уже применяют Claude Code, ChatGPT и другие нейросети в работе, учёбе, бизнесе и жизниПерейти в канал

Как проверить, что API идемпотентен

Про идемпотентность часто спрашивают на собеседовании тестировщика, и её обязательно проверяют в тестах API. Минимальный набор проверок:

  1. Отправить один и тот же запрос дважды и сравнить состояние данных, а не только ответы.
  2. Повторить запрос с тем же ключом и изменённым телом. Ожидается ошибка, старый ответ здесь означает дефект.
  3. Отправить два одинаковых запроса параллельно: операция должна выполниться один раз.
  4. Повторить DELETE уже удалённого ресурса и убедиться, что клиент считает 404 успехом.
  5. Повторить запрос после истечения срока ключа и сверить поведение с документацией.

Все пять сценариев легко автоматизировать: это два-три запроса и проверка таблицы в базе.

Где освоить работу с API системно

Без идемпотентности не обходится ни платёжная интеграция, ни бэкенд-разработка в целом. Рядом с ней стоят коды ответов, авторизация, повторы с паузой, очереди и транзакции.

Собрать это в одну картину помогают курсы, где API проектируют и тестируют на практике. Сравнить программы можно в каталоге курсов по работе с API, а путь в профессию целиком описан в статье «Как стать backend-разработчиком».

КурсШколаСтоимость со скидкойВ рассрочкуДлитель­ностьОбзор курса от Checkroi
Node.js. Профессиональная разработка REST API
Перейти на сайт курса
HTMLAcademyHTML Academy29 900 ₽9960 ₽/мес.2 месяцаОбзор курса
Профессия «Python-разработчик»
Перейти на сайт курса
SkillboxSkillbox157 335 ₽5987 ₽/мес.10 месяцевОбзор курса
Веб-вёрстка
Перейти на сайт курса
SkillboxSkillbox145 881 ₽2818 ₽/мес.3 месяцаОбзор курса
Профессия «Аналитик данных с нуля до middle»
Перейти на сайт курса
НетологияНетология145 600 ₽6066 ₽/мес.12 месяцевОбзор курса
Fullstack-разработчик на Python
Перейти на сайт курса
НетологияНетология137 011 ₽7125 ₽/мес.21 месяцОбзор курса

Больше программ — в полном каталоге курсов по API

Часто задаваемые вопросы

Что значит «идемпотентный» простыми словами?

Операцию можно повторить сколько угодно раз, и результат будет таким же, как после первого выполнения. Кнопка вызова лифта идемпотентна: сколько ни нажимай, приедет один лифт. Кнопка «оплатить» без защиты от повторов нет.

POST идемпотентный или нет?

Нет. По RFC 9110 идемпотентны PUT, DELETE и безопасные методы, а POST в этот список не входит: два одинаковых POST могут создать два заказа. Сделать POST безопасным для повторов можно ключом идемпотентности в заголовке запроса.

PATCH идемпотентный или нет?

Сам по себе нет, так сказано в RFC 5789. Но запрос можно составить идемпотентным: «поставь статус paid» даёт одинаковый результат при любом числе повторов, а «прибавь к балансу 500» нет.

Что такое ключ идемпотентности?

Уникальная строка, обычно UUID версии 4, которую клиент передаёт в заголовке Idempotency-Key. Сервер запоминает ключ и результат операции, а на повтор с тем же ключом возвращает сохранённый ответ вместо нового выполнения. У ЮKassa заголовок называется Idempotence-Key.

Чем идемпотентный метод отличается от безопасного?

Безопасный метод только читает данные и ничего не меняет на сервере: GET, HEAD, OPTIONS, TRACE. Идемпотентный может менять данные, но повтор не добавляет новых изменений: PUT и DELETE. Любой безопасный метод идемпотентен, обратное неверно.

Сколько хранится ключ идемпотентности?

Каждый сервис решает сам. ЮKassa обеспечивает идемпотентность 24 часа после первого запроса, затем повтор обрабатывается как новый. В Stripe для API v1 ключ могут удалить автоматически, когда ему исполнится 24 часа.

Читайте Checkroi первым в Google
Добавьте Checkroi в избранные источники — и наши разборы курсов и обзоры школ будут показываться выше в вашей выдаче Google.
Оставить комментарий
0 комментариев
Форма комментария

Оставьте комментарий

Напишите, что думаете. Нам важно ваше мнение!