Rate limit yang hanya dinyatakan sebagai “100 request per detik” masih menyisakan satu kebijakan penting. Apakah client boleh mengirim 100 request tepat pada awal setiap detik, atau request tersebut harus tersebar merata? Token bucket membuat batas ini eksplisit dengan memisahkan laju berkelanjutan dari kapasitas burst.
Limiter menyimpan saldo token sampai kapasitas tertentu. Token bertambah sesuai refill rate yang dikonfigurasi. Sebuah operasi diterima hanya jika token yang tersedia mencukupi, lalu biaya operasi dikurangkan dari saldo. Waktu idle mengumpulkan kapasitas untuk burst berikutnya, tetapi tidak pernah melewati batas bucket.
Model ini berguna karena laju jangka panjang dan jatah burst jangka pendek menjadi dua kontrol yang terpisah.
Refill rate menetapkan budget berkelanjutan
Misalkan sebuah bucket memiliki kapasitas (B) token dan refill sebesar (r) token per detik. Jika saldo pada waktu (t_0) adalah (T_0), maka sebelum operasi baru dikenai biaya pada waktu (t), saldo dapat dihitung sebagai:
[ T(t) = \min(B,\ T_0 + r(t - t_0)) ]
Operasi dengan biaya (c) diterima ketika (T(t) \ge c). Setelah diterima:
[ T’(t) = T(t) - c ]
Dalam interval panjang, caller yang terus sibuk tidak dapat memakai token lebih cepat daripada refill rate, kecuali token yang sudah tersimpan sebelumnya. Saldo tersimpan itu merupakan jatah burst, bukan kuota berkelanjutan kedua.
Karena itu, bucket tidak memerlukan timer yang benar-benar menambahkan satu token pada setiap tick. Implementasi dapat menyimpan waktu update terakhir dan menghitung kembali saldo yang tersedia ketika traffic datang.
Kapasitas menetapkan burst tersimpan maksimum
Kapasitas (B) membatasi jumlah kredit idle yang dapat terkumpul. Dengan (B = 200) dan (r = 100) token per detik, bucket yang penuh dapat langsung menerima burst dengan biaya total 200 token. Traffic berikutnya kemudian bergantung pada refill sebesar 100 token per detik.
Menaikkan (B) mengubah toleransi burst tanpa mengubah laju berkelanjutan. Menaikkan (r) mengubah budget berkelanjutan sekaligus kecepatan pemulihan bucket yang kosong.
Pemisahan ini penting untuk workload yang memang memiliki burst. Saat user membuka sebuah halaman, beberapa API call dapat muncul bersamaan walaupun rata-rata request user tersebut rendah. Bucket dapat menyerap kelompok request itu tanpa memberikan burst yang sama berulang kali ketika caller terus sibuk.
Biaya operasi tidak harus satu token
Mengenakan satu token per request menganggap semua operasi yang diterima setara. Pendekatan itu tepat hanya jika jumlah request merupakan proksi yang cukup baik untuk resource yang dilindungi.
Limiter dapat menetapkan biaya berbeda. Metadata read kecil mungkin berbiaya satu token, sedangkan export yang mahal memakai lebih banyak. Aritmetika bucket tetap sama; hanya (c) yang berubah.
Biaya berbobot memerlukan kontrak yang stabil. Jika biaya baru diperkirakan setelah banyak kerja sudah dimulai, limiter tidak lagi melindungi kerja tersebut. Sinyal biaya sebaiknya tersedia pada titik admission dan cukup dekat dengan resource yang sedang dibatasi.
Satu bucket juga tidak dapat mewakili semua dimensi resource sekaligus. CPU time, concurrency database, outbound byte, dan kuota tenant dapat memerlukan kontrol terpisah ketika tekanannya tidak bergerak bersama.
Penolakan dan penundaan adalah kebijakan berbeda
Ketika saldo berada di bawah biaya yang dibutuhkan, limiter harus menentukan nasib operasi. Penolakan langsung menjaga queueing tetap berada di luar service yang dilindungi dan memberi caller sinyal overload yang jelas. Penundaan menunggu token berikutnya, sehingga limiter berubah menjadi scheduler dengan queue.
Penundaan memerlukan batas eksplisit. Wait queue tanpa batas dapat mempertahankan semua request tetapi memindahkan overload menjadi konsumsi memory dan tail latency. Batas panjang queue, deadline, atau waktu tunggu maksimum menjaga biaya tersebut tetap terbatas.
Untuk HTTP API, request yang ditolak sering direpresentasikan dengan 429 Too Many Requests. Nilai Retry-After dapat memberi petunjuk retry jika server dapat menghitung jeda yang bermakna. Header tersebut merupakan panduan bagi client; bukan reservasi kapasitas pada masa mendatang.
Timekeeping merupakan bagian dari ketepatan limiter
Aritmetika refill bergantung pada waktu yang berlalu. Limiter lokal pada satu process sebaiknya memakai monotonic clock untuk mengukur durasi agar perubahan wall clock tidak menambah atau menghapus token secara tak terduga.
Enforcement terdistribusi lebih rumit. Dua process independen yang masing-masing memelihara bucket penuh untuk identitas logis yang sama berarti budget yang diizinkan ikut berlipat. Shared atomic state store, deterministic partition ownership, atau skema koordinasi lain diperlukan ketika limit harus berlaku secara global.
Transisi state juga harus atomic pada scope yang dipilih. Request konkuren tidak boleh semuanya melihat saldo sebelum charge yang sama lalu masing-masing membelanjakannya.
Pilihan clock, ownership, dan atomicity karena itu merupakan bagian dari kontrak rate limit, bukan detail implementasi kecil.
Bucket hierarkis dapat menerapkan lebih dari satu batas
Sistem sering memerlukan beberapa limit sekaligus: per-user, per-tenant, per-endpoint, dan service-wide. Sebuah request dapat diterima hanya jika setiap bucket yang berlaku mampu membayar biaya yang ditetapkan.
Komposisi tersebut menimbulkan persoalan atomicity. Jika user bucket sudah dikenai charge lalu service-wide bucket menolak request, sistem harus menentukan apakah charge pertama dikembalikan. Implementasi transactional dapat membuat keputusan multi-bucket menjadi atomic; desain lain dapat menerima kehilangan token secara konservatif atau memakai reservation logic.
Urutan pemeriksaan juga memengaruhi contention dan biaya. Batas lokal yang murah dapat menolak excess yang jelas sebelum limit bersama yang lebih mahal, selama semantik akhirnya sesuai dengan kebijakan yang ditetapkan.
Metric perlu memperlihatkan demand dan state bucket
Jumlah request yang diterima dan ditolak menunjukkan hasil akhir, tetapi tidak memperlihatkan bentuk tekanan. Telemetry yang berguna juga mencakup sisa token, refill rate, kapasitas yang dikonfigurasi, biaya yang dikenakan, waktu tunggu untuk kerja yang ditunda, dan rejection rate berdasarkan limiter key.
Bucket yang terus berada dekat kosong menandakan demand berkelanjutan mendekati atau melewati refill. Bucket yang berulang kali turun dari penuh lalu pulih menandakan demand berbentuk burst. Kedua pola itu memerlukan keputusan kapasitas berbeda meskipun total request serupa.
Metric limiter sebaiknya menghindari label high-cardinality untuk identifier user tanpa batas. Debugging per-tenant dapat memakai sampled trace atau diagnostic channel yang dibatasi, alih-alih menjadikan setiap identitas sebagai metrics series permanen.
Token bucket membuat kontrak admission menjadi konkret
Refill rate menyatakan seberapa cepat izin untuk melakukan kerja kembali tersedia. Kapasitas menyatakan seberapa banyak izin tersebut dapat disimpan untuk burst. Biaya operasi menyatakan seberapa banyak izin yang dipakai satu request.
Parameter itu membuat token bucket lebih presisi daripada angka rate yang ditempelkan pada time window ambigu. Pekerjaan engineering yang tersisa berada pada batas implementasinya: atomic charging, monotonic time, bounded waiting, distributed ownership, dan telemetry yang menunjukkan apakah kebijakan yang dikonfigurasi sesuai dengan load aktual.