Blog · Terbit
Rate limit sebagai masalah rancangan: dua batas yang subjeknya berbeda, satu kode 429 yang berarti tiga hal, dan antrean yang tidak menabrak keduanya
Ada dua batas laju yang diterbitkan di Kasera Pay, dan keduanya sering dibaca sebagai satu hal: 300 request per menit per API key untuk seluruh /v1/*, dan 120 request per menit per IP pada halaman pembayaran publik. Angkanya ada di referensi rate limit. Yang menentukan bentuk integrasi bukan angkanya, melainkan kata terakhir pada masing-masing baris: satu dihitung atas kunci milik penjual, satu lagi dihitung atas alamat IP pembeli. Dua subjek yang berbeda berarti dua kelompok orang yang bisa tertahan, dan yang kedua tidak pernah muncul di log integrasi.
Batas pertama: 300 per menit, dibebankan pada kunci
Setiap panggilan ke /v1/* dihitung terhadap API key yang dipakai, bukan terhadap akun dan bukan terhadap server yang memanggilnya. Membuat tagihan, mengambil satu transaksi, membaca daftar, dan membaca katalog metode pembayaran mengambil jatah dari ember yang sama. Tiga hal ikut dari cara penghitungan itu.
Pertama, kunci mode tes dan kunci mode live adalah dua kunci yang berbeda, jadi jatahnya juga berbeda: gladi bersih yang berat di mode tes tidak memakan jatah penjualan yang sedang berjalan. Kedua, dan sebaliknya, memindahkan beban ke server kedua tidak menambah jatah apa pun selama kedua server memakai kunci yang sama, sehingga menskalakan mendatar justru menghasilkan lonjakan yang lebih rapat karena dua proses menerbitkan tagihan tanpa saling tahu.
Ketiga, 300 per menit berarti rata-rata lima request per detik: longgar untuk lalu lintas pesanan biasa, sempit untuk perulangan yang menerbitkan tagihan dari sebuah daftar tanpa jeda, karena perulangan seperti itu bisa menghabiskan jatah satu menit dalam beberapa detik.
Batas kedua: 120 per menit, dibebankan pada IP pembeli
Halaman pembayaran publik dibatasi per alamat IP, dan itu alamat IP pengunjungnya. Integrasi penjual tidak pernah menyentuh batas ini, karena bukan integrasi yang membuka halaman tersebut. Yang bisa menyentuhnya adalah sekelompok pembeli yang keluar lewat satu alamat IP yang sama.
Bentuk itu lebih sering ada daripada yang terlihat: panitia yang menjual tiket di satu ruangan dengan satu Wi-Fi, kantor yang membayar tagihan dari jaringan kantor, atau sekolah dengan satu sambungan bersama saat pembayaran iuran dibuka serempak. Pada keadaan itu yang tertahan adalah pembeli, dan tidak ada satu pun baris di log penjual yang menjelaskannya. Keluhannya sampai dalam bentuk “halamannya tidak mau dibuka”, bukan dalam bentuk kode galat.
Yang menolong pada kasus itu adalah mengurangi pembukaan halaman per orang, bukan menaikkan angkanya: pada penjualan serentak di satu ruangan, membagi antrean menjadi beberapa gelombang lebih murah daripada memperbaiki apa pun di sisi kode.
Jendelanya tetap, dan dimulai dari request pertama
Jendelanya bukan jendela yang bergeser, dan bukan pula jendela yang mengikuti pergantian menit pada jam dinding. Hitungan dimulai saat request pertama masuk, lalu berumur satu menit sejak saat itu. Dua akibatnya layak dipakai saat menulis logika retry.
Menunggu sampai menit berikutnya bukan strategi yang benar, karena batas menitnya tidak pernah berpapasan dengan jam dinding. Yang benar adalah menunggu sampai satu menit penuh berlalu sejak request pertama pada jendela itu, dan karena saat pastinya tidak diketahui dari luar, jeda yang aman adalah satu menit penuh.
Sebaliknya, jendela tetap membuat lonjakan di perbatasan menjadi mungkin: jatah penuh di ujung satu jendela dan jatah penuh di awal jendela berikutnya sama-sama lolos meskipun jaraknya rapat. Itu bukan celah yang pantas dijadikan rancangan, melainkan peringatan bahwa pengujian beban yang kebetulan jatuh di perbatasan bisa lolos dan memberi rasa aman yang keliru.
Satu kode 429, tiga arti yang berbeda
Bagian ini yang paling sering menghasilkan logika retry yang salah. Referensi kode galat menyebut 429 dan 500 sebagai dua kode yang pantas dicoba ulang, dan itu benar sebagai aturan umum. Tetapi 429 di API ini keluar dari tiga tempat yang berbeda, dan hanya satu di antaranya yang benar-benar selesai dengan menunggu.
Yang pertama adalah batas laju itu sendiri, dengan kode rate_limited. Menunggu menyelesaikannya.
Yang kedua juga rate_limited, tetapi lahir dari pembatas percobaan autentikasi yang gagal, dan pembatas itu dihitung per alamat IP, bukan per kunci. Kunci yang salah atau sudah dicabut, dipakai berulang-ulang dari satu server, akan lebih dulu menabrak pembatas ini daripada memperbaiki apa pun. Gejalanya khas dan mudah salah baca: sesudah variabel lingkungan yang keliru ter-deploy, respons berubah dari 401 unauthorized menjadi 429, sehingga terlihat seperti lalu lintas yang terlalu padat padahal masalahnya kunci. Pesannya menyebut percobaan autentikasi, dan itu pembedanya. Menunggu tidak memperbaiki kunci yang salah.
Yang ketiga adalah jatah akun yang belum selesai verifikasi, dengan kode unverified_count_cap dan unverified_amount_cap. Jatah ini berlaku seumur akun dan tidak pernah reset, jadi menunggu sama sekali tidak mengubah apa pun. Retry yang membacanya sebagai batas laju akan berputar sampai batas percobaannya habis, lalu melaporkan kegagalan yang salah sebabnya. Pada kode ini, logika retry harus berhenti dan memunculkan pesan kepada manusia, karena yang membukanya hanya penyelesaian verifikasi.
Kesimpulan yang bisa langsung dipakai: percabangan retry dibuat atas error.code pada badan respons, bukan atas angka statusnya. Statusnya sama untuk ketiga keadaan, kodenya tidak.
Tidak ada header Retry-After
Respons 429 dari API ini tidak membawa header Retry-After, jadi jeda tunggunya ditentukan sendiri oleh pemanggil. Bentuk yang masuk akal adalah backoff eksponensial dengan sedikit keacakan, misalnya satu detik, dua detik, empat detik, dengan batas atas sekitar satu menit karena jendelanya memang selebar itu, dan dengan jumlah percobaan yang terbatas. Keacakannya bukan hiasan: tanpa itu, sekumpulan proses yang tertahan bersamaan akan mencoba lagi bersamaan juga dan menabrak batas yang sama berulang kali.
Satu hal yang wajib menyertai retry pada pembuatan tagihan: header Idempotency-Key. Percobaan ulang yang dikirim tanpa kunci itu, sesudah permintaan pertama sebenarnya sudah masuk tetapi jawabannya tidak sempat kembali, menghasilkan dua permintaan pembayaran untuk satu pesanan. Kunci itu satu-satunya yang mencegahnya, karena external_id dan merchant_ref hanya label yang disimpan dan dikembalikan. Rinciannya dibahas di tulisan tentang idempotensi pada pembuatan tagihan, dan kasus dua baris yang terlanjur lahir dibahas di tulisan tentang pembayaran yang masuk dua kali.
Bentuk antrean untuk penerbitan tagihan massal
Kasus yang paling sering menabrak batas per kunci adalah penerbitan tagihan serempak: iuran bulanan satu komunitas, tagihan sewa, atau undangan pembayaran ke seluruh peserta sebuah acara. Susunan berikut menahan diri di bawah batas tanpa perlu menghitung ember apa pun.
- Jangan kirim langsung dari perulangan. Masukkan daftarnya ke antrean, lalu jalankan antrean itu dengan satu pekerja. Satu pekerja membuat laju menjadi sesuatu yang diatur, bukan sesuatu yang kebetulan.
- Pasang jeda tetap sekitar 250 milidetik antar pembuatan. Hasilnya sekitar empat request per detik, yaitu 240 per menit, dan menyisakan ruang untuk lalu lintas normal yang berjalan di saat yang sama pada kunci yang sama.
- Sediakan satu
Idempotency-Keyyang stabil dan tersimpan untuk tiap baris, misalnya gabungan nomor pelanggan dan periode tagihan. Kunci itu harus bertahan melewati restart pekerja, karena kunci yang lahir baru setiap percobaan tidak melindungi apa pun. - Perlakukan
429sebagai sinyal untuk memperlambat seluruh antrean, bukan hanya untuk mengulang satu baris. Menaikkan jeda ke satu detik sesudah penolakan pertama lebih cepat selesai daripada mengulang baris demi baris pada laju yang sama. - Catat
request_iddari badan respons galat pada baris yang gagal. Nilai itu yang membuat satu kejadian bisa ditelusuri tanpa menebak.
Satu catatan waktu khusus untuk penerbitan massal: masa berlaku berjalan sejak tagihan dibuat, bukan sejak dikirim. Seribu tagihan yang diterbitkan pukul delapan pagi lalu dikirim bertahap sampai siang sebagian sudah berumur beberapa jam saat pertama kali dibaca. Bawaan enam puluh menit dapat diperpanjang lewat expires_in_minutes saat pembuatan.
Yang bukan rate limit, meskipun ikut menolak
Dua hal lain sering ditaruh dalam satu keranjang dengan batas laju, padahal penanganannya berbeda. Batas nominal berlaku per pembayaran dan dicek saat pembuatan: QRIS mulai Rp 1.000, Virtual Account mulai Rp 10.000, dengan maksimum Rp 10.000.000, ditolak sebagai 422 amount_too_small atau 422 amount_too_large. Nilainya bisa berbeda per akun dan selengkapnya ada di halaman batas transaksi. Penolakan 422 tidak pernah membaik dengan diulang.
Batas jumlah tagihan per hari tidak ada. Akun yang sudah terverifikasi tidak dibatasi jumlah maupun total nominal permintaan pembayaran per hari, sehingga yang tersisa sebagai penghambat pada hari yang ramai memang hanya laju per menit yang dibahas di sini.
Batas laju juga tidak berlaku pada arah sebaliknya: pengiriman webhook berjalan menuju endpoint penerima, jadi yang membatasi di sana adalah kapasitas endpoint itu sendiri. Tangga percobaan ulangnya dibahas di tulisan tentang webhook yang tidak sampai.
Ringkasnya
- 300 per menit dihitung per API key, 120 per menit dihitung per IP pembeli. Yang kedua bisa menahan pembeli tanpa meninggalkan jejak di log penjual.
- Jendelanya tetap dan dimulai dari request pertama, jadi menunggu pergantian menit bukan strategi yang benar.
429keluar dari tiga tempat. Percabangan retry dibuat ataserror.code, bukan atas angka statusnya.- Tidak ada
Retry-After. Backoff dan keacakannya ditentukan sendiri, dan retry pembuatan tagihan tanpaIdempotency-Keyyang stabil adalah cara menagih dua kali. - Penerbitan massal dijalankan satu pekerja dengan jeda tetap, bukan perulangan tanpa jeda.