Request ganda adalah salah satu masalah yang sering baru terlihat setelah aplikasi digunakan dalam kondisi nyata. Pengguna menekan tombol bayar dua kali, koneksi terputus setelah server menerima request, atau aplikasi mobile otomatis mengulang request karena mengira permintaan sebelumnya gagal. Jika endpoint tidak dirancang untuk menghadapi kondisi ini, satu tindakan pengguna dapat menghasilkan dua pesanan, dua tagihan, atau dua perubahan data.
Di sinilah konsep idempotensi menjadi penting. Sederhananya, operasi idempoten adalah operasi yang tetap menghasilkan keadaan akhir yang sama meskipun request yang sama dikirim lebih dari sekali. Konsep ini sangat berguna untuk API, sistem pembayaran, proses checkout, webhook, dan pekerjaan asinkron.
Mengapa request bisa terkirim lebih dari sekali?
Pengulangan request tidak selalu berarti pengguna melakukan kesalahan. Ada beberapa penyebab yang umum terjadi:
- Pengguna menekan tombol submit berulang kali karena halaman terlihat tidak merespons.
- Koneksi internet terputus setelah request sampai ke server, tetapi sebelum respons kembali ke browser.
- Client melakukan retry otomatis ketika menerima timeout atau error jaringan.
- Load balancer, message queue, atau worker menjalankan ulang pekerjaan yang dianggap gagal.
- Webhook dari layanan eksternal dikirim ulang karena pengirim belum menerima respons sukses.
Masalahnya, server sering tidak bisa membedakan antara request baru dan pengulangan request lama. Dua request yang memiliki isi sama belum tentu dianggap sebagai operasi yang sama. Karena itu, aplikasi membutuhkan penanda khusus yang disebut idempotency key.
Apa itu idempotency key?
Idempotency key adalah nilai unik yang dibuat oleh client untuk menandai satu operasi. Nilai ini dikirim melalui header HTTP, misalnya Idempotency-Key: 7f2c.... Server kemudian menyimpan key tersebut bersama hasil pemrosesan request.
Jika request dengan key yang sama datang lagi, server tidak memproses operasi dari awal. Server cukup mengembalikan hasil yang sebelumnya sudah disimpan. Dengan begitu, client tetap bisa melakukan retry tanpa membuat data baru.
Bayangkan Anda memesan makanan melalui loket. Nomor pesanan bukan sekadar tanda antrean, tetapi juga identitas transaksi. Jika Anda bertanya dua kali menggunakan nomor pesanan yang sama, petugas tidak membuat pesanan baru. Ia hanya menunjukkan status pesanan yang sudah ada.
Contoh alur sederhana
Misalnya sebuah aplikasi memiliki endpoint untuk membuat pesanan:
POST /api/ordersClient membuat key unik sebelum mengirim request:
Idempotency-Key: order-8b2d9e21Server kemudian menjalankan alur berikut:
- Memeriksa apakah key tersebut sudah tersimpan.
- Jika belum ada, server memulai transaksi dan menyimpan key dengan status
processing. - Server membuat pesanan, menyimpan hasilnya, lalu mengubah status menjadi
completed. - Jika key yang sama dikirim ulang, server mengembalikan hasil pesanan yang sudah ada.
Dalam praktiknya, key sebaiknya dibuat oleh client untuk satu tindakan, bukan dibuat ulang setiap kali retry. Jika aplikasi membuat key baru pada setiap percobaan, server akan menganggap setiap request sebagai operasi berbeda.
Jangan hanya memeriksa key
Menyimpan idempotency key saja belum cukup. Server juga perlu memastikan bahwa key tersebut tidak digunakan untuk isi request yang berbeda. Misalnya, key order-8b2d9e21 pertama kali digunakan untuk membeli produk A, lalu dipakai lagi untuk produk B. Kondisi ini seharusnya ditolak karena dapat menimbulkan perilaku yang membingungkan.
Salah satu cara yang umum adalah menyimpan hash dari bagian penting request, seperti user ID, nominal, mata uang, dan detail pesanan. Ketika key yang sama digunakan lagi, server membandingkan hash tersebut. Jika berbeda, server mengembalikan error seperti 409 Conflict.
Contoh struktur data yang dapat disimpan:
idempotency_key: order-8b2d9e21
user_id: 42
request_hash: a91f...
status: completed
response_code: 201
response_body: {...}
expires_at: 2026-09-16 10:00:00Bagaimana menerapkannya di PHP dan MySQL?
Untuk aplikasi berbasis PHP dan MySQL, Anda dapat membuat tabel khusus untuk mencatat operasi idempoten:
CREATE TABLE idempotency_keys (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT UNSIGNED NOT NULL,
idempotency_key VARCHAR(100) NOT NULL,
request_hash CHAR(64) NOT NULL,
status VARCHAR(20) NOT NULL,
response_code INT NULL,
response_body JSON NULL,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
UNIQUE KEY unique_user_key (user_id, idempotency_key)
);Index unik penting karena membantu database mencegah dua proses memasukkan key yang sama secara bersamaan. Pemeriksaan di level kode saja memiliki celah race condition, yaitu dua request membaca kondisi “belum ada” pada waktu yang hampir bersamaan lalu sama-sama membuat transaksi.
Gunakan transaksi database untuk menggabungkan pencatatan key dan pembuatan data bisnis. Jika proses gagal, status dapat diubah menjadi failed atau catatan idempoten dibersihkan sesuai kebutuhan. Namun, jangan menghapusnya sembarangan jika penghapusan dapat menyebabkan retry memproses transaksi ulang.
Bagaimana menangani status processing?
Situasi yang lebih rumit terjadi ketika request pertama masih diproses, lalu request kedua dengan key yang sama masuk. Server perlu memiliki aturan yang jelas. Beberapa pilihan yang dapat digunakan:
- Mengembalikan
409 Conflictdengan pesan bahwa operasi masih berjalan. - Membuat client menunggu sebentar lalu mencoba lagi.
- Mengembalikan status pekerjaan jika proses dilakukan secara asinkron.
- Menggunakan lock dengan batas waktu agar proses yang macet tidak mengunci key selamanya.
Jangan membiarkan status processing tanpa batas waktu. Jika worker mati setelah mencatat status tersebut, request berikutnya bisa terus dianggap sedang diproses. Simpan waktu pembaruan terakhir dan siapkan mekanisme pemulihan.
Idempotensi tidak sama dengan mencegah semua duplikasi
Idempotency key melindungi satu operasi yang sama dari pengulangan. Namun, fitur ini tidak otomatis mencegah pengguna membuat dua pesanan berbeda dengan menekan tombol beli pada waktu yang berbeda. Jika kebutuhan bisnisnya adalah mencegah pesanan identik dalam rentang waktu tertentu, Anda tetap membutuhkan aturan tambahan, seperti validasi status keranjang atau batasan duplikasi.
Idempotensi juga berbeda dari validasi input. Validasi memastikan data yang masuk benar, sedangkan idempotensi memastikan pengulangan operasi tidak menghasilkan efek samping tambahan.
Yang bisa dilakukan sekarang
- Identifikasi endpoint yang menimbulkan efek samping, seperti membuat pesanan, pembayaran, pengiriman, atau perubahan saldo.
- Tambahkan dukungan header idempotency key pada endpoint tersebut.
- Buat tabel pencatatan dengan index unik di database.
- Simpan hash request agar key tidak dapat digunakan untuk isi data yang berbeda.
- Uji skenario timeout, retry, klik ganda, dan dua request bersamaan.
- Tentukan masa simpan key berdasarkan risiko operasi. Proses pembayaran biasanya membutuhkan masa simpan lebih panjang daripada formulir kontak.
Dalam aplikasi web, retry bukan sesuatu yang bisa dihindari sepenuhnya. Jaringan tidak selalu stabil, browser bisa mengirim ulang request, dan sistem terdistribusi dapat menjalankan pekerjaan lebih dari sekali. Dengan merancang endpoint agar idempoten sejak awal, aplikasi menjadi lebih aman menghadapi kondisi nyata—bukan hanya saat semuanya berjalan sempurna.
– Rio Yotto @rioyotto
