# Pola Idempotent Consumer untuk Message Queue

> Broker Anda mengirim setidaknya sekali, jadi duplikat itu pasti terjadi. Ini cara tabel processed-messages mengubahnya menjadi efek exactly-once.

URL: https://www.matthewswong.com/id/blog/idempotent-consumer-pattern-message-queue/

- Author: Matthews Wong (https://www.matthewswong.com/id/about/)
- Published: 2026-07-11
- Updated: 2026-07-11
- Category: Backend

> **Key takeaway:** Message broker menjamin pengiriman setidaknya sekali, jadi pesan yang sama pada akhirnya akan tiba dua kali. Pola idempotent consumer mengatasinya: catat setiap ID pesan yang diproses di tabel database dengan unique constraint, dan jalankan insert itu plus efek sampingnya dalam satu transaksi. Duplikat menabrak constraint, rollback, dan tidak mengubah apa pun.

Setiap message queue yang pernah saya jalankan di produksi, dari RabbitMQ ke Kafka ke SQS, membuat janji yang sama: pengiriman setidaknya sekali. Bukan exactly-once. Kata itu penting. Artinya broker akan mengirim ulang pesan jika tidak yakin Anda selesai memprosesnya, dan sering kali memang tidak yakin. Consumer crash setelah menyelesaikan pekerjaan tetapi sebelum acknowledge. Gangguan jaringan menjatuhkan ack. Visibility timeout habis saat Anda masih sibuk. Dalam setiap kasus, broker melakukan hal yang aman dan menyerahkan pesan itu lagi.

Jadi duplikat bukan kasus tepi yang mungkin Anda alami. Itu kepastian yang harus Anda antisipasi dalam desain. Jika consumer Anda menagih kartu, mengirim email, atau mengurangi stok, memproses pesan yang sama dua kali adalah bug nyata dengan uang nyata di dalamnya. Pola idempotent consumer adalah jawaban standar, membosankan, dan benar, dan hampir seluruhnya berada di database Anda.

## Mengapa pengiriman exactly-once itu mitos

Pengiriman exactly-once melintasi jaringan mustahil dijamin, karena acknowledgement itu sendiri bisa hilang. Jika consumer melakukan ack dan ack itu lenyap, broker harus memilih: kirim ulang dan berisiko duplikat, atau buang dan berisiko kehilangan pesan selamanya. At-least-once memilih yang pertama, satu-satunya pilihan aman. Yang dipasarkan vendor sebagai exactly-once sebenarnya adalah pengiriman at-least-once plus pemrosesan idempotent di sisi consumer. Semantik exactly-once Kafka bekerja di dalam batas Kafka; begitu Anda menulis ke Postgres atau memanggil Stripe, Anda kembali membutuhkan idempotensi Anda sendiri.

> Idempotent berarti menjalankan operasi N kali meninggalkan sistem dalam keadaan yang sama seperti menjalankannya sekali. Itulah properti yang Anda rekayasa, bukan nol duplikat. Anda tidak bisa mencegah duplikat tiba; Anda bisa membuatnya tidak berbahaya.

## Tabel processed-messages

Seluruh pola bertumpu pada satu tabel. Setiap baris adalah sidik jari pesan yang sudah Anda tangani. Kolom message ID membawa unique constraint, yang merupakan elemen penopang seluruh desain. Ketika duplikat tiba, insert melanggar constraint itu dan gagal dengan jelas, dan kegagalan itulah sinyal yang Anda inginkan.

Dua catatan desain. Pertama, gunakan business message ID sebagai kunci, bukan offset broker atau delivery tag. Offset direset, tag bersifat per-koneksi; ID stabil yang ditetapkan produser bertahan melalui pengiriman ulang dan rebalancing. Kedua, sertakan nama consumer dalam kunci ketika beberapa consumer berbeda berlangganan topik yang sama, sehingga masing-masing mendapat jaminan sekali-saja sendiri.

## Satu transaksi adalah inti triknya

Kesalahan halusnya adalah mengecek tabel, lalu memproses, lalu insert. Itu menyisakan celah: dua pengiriman duplikat bisa sama-sama membaca kosong, sama-sama memproses, sama-sama insert. Bentuk yang benar membungkus insert dedup dan efek samping bisnis dalam satu transaksi database. Jika insert berhasil, kerjakan pekerjaan dan commit. Jika insert menabrak unique constraint, pesan itu duplikat, jadi rollback dan acknowledge tanpa melakukan apa pun. Atomisitas itulah yang membuatnya aman di bawah konkurensi.

> Ini hanya berlaku ketika efek samping dan baris dedup berbagi satu penyimpanan transaksional. Jika efek samping Anda adalah panggilan API eksternal, transaksi database tidak bisa me-rollback-nya. Itu masalah berbeda, dibahas di bawah.

## Ketika efek samping keluar dari database Anda

Pembaruan baris lokal me-rollback dengan bersih. Menagih kartu lewat Stripe tidak; begitu panggilan HTTP itu berhasil, uang sudah berpindah, dan tidak ada rollback transaksi yang membatalkan penagihannya. Di sini Anda mendorong idempotensi ke pihak ketiga. Sebagian besar API pembayaran dan pesan yang serius menerima idempotency key, dan IETF memiliki draft Standards Track aktif untuk header HTTP Idempotency-Key, revisi 07 per akhir 2025. Kirim kunci stabil yang diturunkan dari message ID; penyedia melakukan deduplikasi di sisi mereka dan mengembalikan hasil asli saat retry.

- Turunkan idempotency key secara deterministik dari message ID, sehingga pengiriman ulang menghasilkan kunci yang persis sama.
- Simpan hasil panggilan eksternal dalam transaksi Anda sebelum commit, sehingga Anda tidak pernah mengulang panggilan pada pengiriman ulang berikutnya.
- Urutkan operasi agar efek eksternal menjadi hal terakhir sebelum commit, memperkecil jendela di mana crash meninggalkan pekerjaan setengah jadi.

## Pembersihan, pertumbuhan, dan detail yang menggigit

Tabel processed-messages tumbuh selamanya jika Anda biarkan. Anda tidak butuh baris yang lebih tua dari jendela pengiriman ulang maksimum broker Anda, biasanya jam, bukan bulan. Pangkas secara terjadwal dan tabel tetap kecil serta indeks uniknya tetap cepat.

| Keputusan | Cara salah | Cara benar |
| --- | --- | --- |
| Kunci dedup | Offset broker atau delivery tag | Business message ID dari produser |
| Cek vs insert | SELECT, lalu proses, lalu INSERT | INSERT dulu, biarkan unique constraint yang menentukan |
| Lingkup transaksi | Baris dedup dan efek samping di commit terpisah | Keduanya dalam satu transaksi atomik |
| Panggilan eksternal | Anggap transaksi DB melindunginya | Kirim idempotency key ke penyedia |
| Pertumbuhan tabel | Simpan setiap baris selamanya | Pangkas melewati jendela pengiriman ulang |

> Sebagian besar operasi yang secara alami idempotent tidak butuh tabel sama sekali. Menyetel status ke PAID atau meng-upsert record berdasarkan kunci sudah aman diulang. Gunakan tabel processed-messages ketika operasi bersifat akumulatif, seperti menambah saldo atau menambahkan entri ledger.

Digabungkan, polanya tidak glamor dan itulah intinya. Asumsikan pengiriman at-least-once, gunakan message ID yang stabil sebagai kunci, insert ke tabel dengan unique constraint dalam transaksi yang sama dengan efek samping Anda, dorong idempotensi ke sistem eksternal mana pun, dan pangkas baris lama. Begitulah Anda membangun efek exactly-once di atas broker yang hanya menjanjikan at-least-once, dan bentuknya sama baik Anda menjalankan Kafka, RabbitMQ, maupun SQS.

## Sources

- Microservices.io — Pattern: Idempotent Consumer
- Milan Jovanovic — The Idempotent Consumer Pattern
- Event-Driven.io — Outbox, Inbox patterns and delivery guarantees
- IETF — The Idempotency-Key HTTP Header Field (draft-ietf-httpapi-idempotency-key-header-07)

## Source links

- https://microservices.io/patterns/communication-style/idempotent-consumer.html
- https://milanjovanovic.tech/blog/the-idempotent-consumer-pattern-in-dotnet-and-why-you-need-it
- https://event-driven.io/en/outbox_inbox_patterns_and_delivery_guarantees_explained/
- https://datatracker.ietf.org/doc/html/draft-ietf-httpapi-idempotency-key-header-07

## FAQ

### Apa itu pola idempotent consumer?

Ini cara membuat consumer message-queue aman terhadap pengiriman duplikat. Anda mencatat ID setiap pesan yang diproses di tabel database dengan unique constraint, dan insert itu dilakukan dalam transaksi yang sama dengan pekerjaan sebenarnya. Jika pesan yang sama tiba lagi, insert gagal pada unique constraint, sehingga Anda melewati pekerjaan dan meng-acknowledge duplikat tanpa dampak.

### Mengapa message queue tidak bisa mengirim exactly-once saja?

Karena acknowledgement bisa hilang di perjalanan. Jika consumer menyelesaikan pekerjaan dan mengirim ack yang tidak pernah tiba, broker tidak bisa membedakan consumer yang lambat dari yang mati. Untuk menghindari kehilangan pesan, broker mengirim ulang, itulah pengiriman at-least-once. Exactly-once sejati melintasi jaringan terbukti mustahil, jadi consumer harus menangani duplikat sendiri.

### Sebaiknya tabel dedup dikunci pada offset broker atau message ID?

Gunakan business message ID yang stabil dan ditetapkan produser, bukan offset broker atau delivery tag. Offset bisa direset dan delivery tag hanya berlaku untuk satu koneksi, jadi keduanya tidak bertahan andal melalui pengiriman ulang atau rebalancing consumer. Message ID yang disetel produser tetap konstan di setiap pengiriman ulang, persis yang dibutuhkan deduplikasi.

### Bagaimana tetap idempotent ketika efek sampingnya adalah panggilan API eksternal?

Transaksi database tidak bisa me-rollback panggilan HTTP yang sudah berhasil, jadi Anda mendorong idempotensi ke penyedia. Kirim idempotency key yang diturunkan secara deterministik dari message ID; sebagian besar API pembayaran dan pesan melakukan deduplikasi pada kunci itu dan mengembalikan hasil asli saat retry. IETF juga memiliki draft Standards Track aktif untuk header HTTP Idempotency-Key.

### Bukankah tabel processed-messages akan tumbuh selamanya?

Ya jika tidak pernah dibersihkan. Anda hanya butuh baris yang lebih baru dari jendela pengiriman ulang atau retensi maksimum broker, biasanya dalam hitungan jam. Jalankan delete terjadwal yang menghapus baris lebih tua dari margin aman, dan tabel beserta indeks uniknya tetap kecil dan cepat.

---

- HTML version: https://www.matthewswong.com/id/blog/idempotent-consumer-pattern-message-queue/
- English: https://www.matthewswong.com/en/blog/idempotent-consumer-pattern-message-queue/
- Site index for agents: https://www.matthewswong.com/llms.txt
- Sitemap: https://www.matthewswong.com/sitemap.xml
- API description (OpenAPI): https://www.matthewswong.com/openapi.json
