---
title: "Idempotency: Aynı Mesaj İki Kez Geldiğinde"
description: "At-least-once teslimat, bir handler'ın aynı mesajı iki kez göreceği anlamına gelir. Idempotency anahtarı, nereden geldiği ve çökmeden sağ çıkan dedupe."
url: https://sade.dev/tr/notes/idempotency-ayni-mesaj-iki-kez/
lang: tr
author: "Muhammet Şafak"
published: 2026-06-17
updated: 2026-09-06
section: Note
tags: ["queue","reliability","architecture","async"]
summary: "At-least-once yok edilecek bir arıza değil, etrafında tasarım yapılacak bir garantidir. Anahtarı gönderici basar ve yalnızca payload'dan türetilmez; işi yapmakla anahtarı kaydetmek arasındaki boşluk da şekli belirler: etki senin kontrolündeyse anahtarı aynı transaction'da yaz, değilse seen-set'le yetin. Eşzamanlı mükerrerler için kontrol değil, unique constraint gerekir."
---

# Idempotency: Aynı Mesaj İki Kez Geldiğinde

> At-least-once yok edilecek bir arıza değil, etrafında tasarım yapılacak bir garantidir. Anahtarı gönderici basar ve yalnızca payload'dan türetilmez; işi yapmakla anahtarı kaydetmek arasındaki boşluk da şekli belirler: etki senin kontrolündeyse anahtarı aynı transaction'da yaz, değilse seen-set'le yetin. Eşzamanlı mükerrerler için kontrol değil, unique constraint gerekir.

Bir müşteri tek sipariş için iki kez ücretlendirildi. Hiçbir şey bilerek iki kez çalışmadı: ödeme worker'ı kartı çekti, sonra mesajı ack'leyemeden çöktü — broker da sözünü tutup mesajı yeniden teslim etti.

Hata, mükerrer teslimat değildi. Mükerrer teslimat için inşa edilmemiş handler'dı.

## At-least-once bir söz, bir arıza değil

Neredeyse her queue — ve yeniden denenen her HTTP çağrısı — **at-least-once**'tır. Bir handler aynı mesajı birden çok kez görebilir: bir çökme, bir release, bir redelivery ya da timeout'a düşüp yeniden deneyen bir istemci sonrası. Ağ üzerinde exactly-once, broker'ın ucuza veremeyeceği bir koordinasyon olmadan pratik değil; bu yüzden çok az broker onu vaat eder — vaat edenler de (Google Cloud Pub/Sub gibi) dar koşullara sıkıştırır: tek bölge, yalnızca pull aboneliği.

Bu, problemi yeniden çerçeveler. Redelivery, yok edilecek bir başarısızlık değil; etrafında tasarım yapılacak bir garanti. ([Queue](/tr/notes/senkron-mu-asenkron-mu)'nun başarısız bir job'u kaybetmek yerine yeniden denerken yaptığı pazarlığın aynısı.)

## Idempotent demek "iki kez = bir kez" demek

Bir operasyon, defalarca uygulandığında etkisi bir kez uygulanmışla aynıysa idempotent'tir — `f(f(x)) = f(x)`.

- "Sipariş durumunu `paid` yap" idempotent'tir.
- "Bakiyeyi 10 artır" **değildir** — iki kez +20 eder.
- "Karşılama e-postasını gönder" **değildir** — iki kez iki e-posta eder.

Bütün iş, ikinci tür operasyonu birinciye çevirmek.

## Idempotency anahtarı göndericiden gelir

*Bu operasyon* için kararlı bir kimliğe ihtiyacın var ve bunu **gönderici** belirlemeli, alıcı değil:

- Bir queue mesajı için bu, **üreticinin ürettiği mesaj-başına id**'dir — mesaj başına bir id, trace/correlation id'den ayrı (o, birçok mesajı kapsar).
- Bir HTTP yazımı için bu, **istemcinin ürettiği `Idempotency-Key` başlığı**'dır — Stripe modeli.

İki kural anahtarı dürüst tutar:

- **Yalnızca payload'dan türetme.** Meşru biçimde birbirinin aynı iki istek — aynı müşterinin aynı ürünü iki kez alması — çakışır ve ikincisi sessizce düşürülür.
- **Anahtarı alıcıya ürettirme.** Alıcı, bir retry'ı yepyeni bir çağrıdan ayıramaz; "bu, daha önce denediğim aynı operasyon" bilgisi yalnızca göndericide vardır.

## Dedupe: zaten yaptığını hatırla

Handler, işten önce tek bir kontrol yapar: *bu anahtar işlendi mi?* Evetse atla ve ack'le. Hayırsa işi yap, sonra anahtarı kaydet. Tüm desen bu — ağırlık, kaydın *nerede* durduğunda.

## İş ile kayıt arasındaki çökme

Zor kısım, işi yapmakla anahtarı kaydetmek arasındaki boşluk. İki şekil var ve doğrusu yan etkiye bağlı:

- **Seen-set — başarıdan sonra kaydet.** Handler döndükten sonra anahtarı Redis'e ya da bir tabloya yaz. Ucuz ve geniş çapta uygulanabilir. Pencere: yan etkiden *sonra* ama kayıttan *önce* çökersen, bir redelivery yeniden işler. Yan etkinin kendisi idempotent'se sorun değil — bir `UPSERT`, bir "set to paid".
- **Transactional — işle birlikte kaydet.** Idempotency kaydını, iş değişikliğiyle **aynı veritabanı transaction'ında** yaz. Pencere yok: anahtar ve etki birlikte commit olur ya da birlikte geri alınır. Bedeli: etki, senin kontrol ettiğin bir DB yazımı olmalı ve anahtar deposu aynı veritabanı olmalı — ayrı bir Redis değil. Kendi kontrolündeki bir etki için mükerrer işlemeyi gerçekten bitiren budur.

Yani yan etki senin yerine seçer: sahip olduğun bir satır → transactional; transaction'ına dahil edemeyeceğin üçüncü-taraf bir çağrı (e-posta, bir çekim) → seen-set ve kendi anahtarını onun `Idempotency-Key`'i olarak ileterek downstream'i de idempotent yap.

## Eşzamanlı mükerrerlere kontrol değil, constraint lazım

Aynı anahtarın, ikisi de kaydetmeden yarışan iki teslimatı bir "önce kontrol et sonra yaz" dizisinden sıyrılır — o dizi atomik değil. **Anahtar kolonundaki bir unique constraint** atomiktir: ikinci insert, yakalayıp "zaten işlendi" diye ele aldığın bir conflict'e dönüşür. Bu, [sıralı numara üretiminde bir race'in açtığı gap'lerin](/tr/systems/ardisik-numara-uretiminde-race-condition-ve-gap) aynı şekli — benzersizlik gerçekte uygulamada değil, veritabanında zorlanır.

## Buna ne zaman hiç ihtiyacın yok?

Handler zaten idempotent'se — kararlı bir id ile anahtarlanmış saf bir `UPSERT`, bir "alanı şu değere ayarla" — hiçbir şeye ihtiyacın olmayabilir. Anahtar + dedupe'a, operasyonun **idempotent olmayan bir yan etkisi** (para, e-posta, dış bir `POST`) ya da **birikimi** (increment, append) olduğunda uzan. Karmaşıklığı yalnızca ikinci çalıştırmanın gerçekten zarar verdiği yere harca.

---

At-least-once, düzelteceğin kısım değil; kabul ettiğin kısım. Handler'ı, ikinci teslimat bir no-op olacak şekilde kur; o zaman redelivery bir olay olmaktan çıkar ve olduğu şeye döner — broker'ın sözünü tutması.

---

*İlgili:* [BabelQueue idempotency spec'i](https://babelqueue.com/docs/spec/1.x/idempotency) anahtarın nereden geldiğini ve dedupe'un nasıl davrandığını sabitler; [`idempotency-payments` örneği](https://github.com/BabelQueue/babelqueue-examples/tree/main/idempotency-payments) ise tam da bu çift-çekim vakasının çalıştırılabilir hâlidir.

## Sık sorulanlar

**Idempotency anahtarını kim üretmeli — gönderici mi, alıcı mı?**

Bunu gönderici belirlemeli, alıcı değil. Bir queue mesajı için bu, üreticinin ürettiği mesaj-başına id'dir; bir HTTP yazımı için bu, istemcinin ürettiği Idempotency-Key başlığıdır — Stripe modeli. Alıcı bir retry'ı yepyeni bir çağrıdan ayıramaz; "bu, daha önce denediğim aynı operasyon" bilgisi yalnızca göndericide vardır.

**Handler bir mesajın daha önce işlendiğini nasıl anlar?**

Handler, işten önce tek bir kontrol yapar: bu anahtar işlendi mi? Evetse atla ve ack'le. Hayırsa işi yap, sonra anahtarı kaydet. Tüm desen bu — ağırlık, kaydın nerede durduğunda.


## Kaynaklar

- [RFC 9110, 9.2.2: Idempotent Methods](https://www.rfc-editor.org/rfc/rfc9110.html#name-idempotent-methods) — IETF
- [Stripe API: idempotent requests](https://docs.stripe.com/api/idempotent_requests) — Stripe
- [Exactly-once delivery: pull subscriptions, single region](https://docs.cloud.google.com/pubsub/docs/exactly-once-delivery) — Google Cloud
- [Consumer acknowledgements and publisher confirms](https://www.rabbitmq.com/docs/confirms) — RabbitMQ
