Token Bucket Menjaga Kapasitas Burst Tanpa Menghapus Batas Rate

Batas requests-per-second yang kaku memperlakukan lonjakan singkat dan banjir traffic berkelanjutan sebagai kejadian yang sama. Pendekatan itu dapat terlalu ketat untuk service dengan caller yang secara alami mengirim request dalam kelompok. Token bucket memisahkan dua batas: admission rate jangka panjang dan jumlah burst traffic yang bersedia diserap service.

Model ini memiliki dua parameter. Kapasitas bucket B adalah jumlah token maksimum yang dapat terkumpul. Refill rate r menambahkan token per satuan waktu hingga mencapai B. Sebuah request memakai token sesuai cost yang dikonfigurasi. Jika token mencukupi, request diteruskan; jika tidak, request ditolak, ditunda, atau ditangani oleh policy eksplisit lain.

capacity B = 100 token
refill r   = 20 token/s
cost       = 1 token/request

Setelah lima detik tanpa traffic, bucket dapat menyimpan paling banyak 100 token. Caller dapat memakai 100 token itu dengan cepat, tetapi traffic berikutnya hanya dapat diteruskan seiring token kembali pada rate 20 per detik.

Burst allowance adalah kapasitas tersimpan yang terbatas

Token mewakili admission credit yang tersimpan selama periode lebih sepi. Token tidak menambah kapasitas fisik service. Fungsinya adalah memberi caller hak memakai sejumlah fleksibilitas admission yang sudah ditetapkan.

Untuk request dengan cost satu token, transisi state dapat ditulis sebagai:

tokens = min(B, tokens + elapsed * r)

if tokens >= 1:
    tokens -= 1
    admit()
else:
    reject()

Batas B sangat penting. Tanpanya, client yang lama idle dapat mengumpulkan credit tanpa batas lalu mengirim burst sebesar apa pun. Dengan bucket terbatas, waktu idle berhenti menambah burst entitlement setelah bucket penuh.

Bucket yang dimulai dalam kondisi penuh juga mengizinkan burst langsung setelah dibuat atau di-reset. Memulainya dari kondisi kosong menghasilkan perilaku startup yang berbeda. Pilihan tersebut merupakan bagian dari traffic contract, bukan sekadar detail implementasi.

Refill rate mengendalikan envelope berkelanjutan

Ambil bucket dengan B = 60 dan r = 10 token/s. Client dapat mengirim 60 request dengan cost satu token secara langsung ketika bucket penuh. Jika traffic berlanjut pada 10 request per detik, bucket dapat tetap hampir kosong sementara request terus lolos saat token baru tersedia.

Jika client terus mengirim 25 request per detik, demand melampaui refill sebesar 15 token per detik. Saldo tersimpan akan habis, kemudian request berlebih akan bertemu limiter.

full bucket
60 |\
   | \
   |  \   offered rate > refill rate
 0 +---\------------------------> time
        sustained admission ~= r

Burst budget mengubah perilaku sementara. Budget tersebut tidak mengubah replenishment rate jangka panjang.

Perhitungan waktu memerlukan basis monotonic

Token bucket bergantung pada elapsed time sehingga penanganan clock merupakan bagian dari correctness. Pengukuran durasi sebaiknya memakai monotonic clock jika runtime menyediakannya. Penyesuaian wall clock dapat menggeser civil time maju atau mundur dan tidak seharusnya menambah atau mengurangi admission credit.

Implementasi tidak memerlukan timer yang memasukkan token pada setiap tick. Lazy refill biasanya lebih sederhana: simpan titik update terakhir dan saldo token, lalu hitung replenishment ketika request tiba.

elapsed = now_monotonic - last_update
tokens = min(B, tokens + elapsed * r)
last_update = now_monotonic

Cara ini menghindari pekerjaan background timer untuk bucket yang tidak aktif dan membuat transisi state tetap berada di jalur admission.

Representasi numerik juga perlu diperhatikan. Fractional token dapat disimpan dengan fixed-point arithmetic atau tipe numerik yang cukup presisi. Aturan pembulatan tidak boleh secara sistematis menciptakan credit tambahan setelah update berulang.

Request cost dapat mewakili beban yang berbeda

Menghitung setiap request sebagai satu token hanya tepat ketika dampaknya terhadap resource yang dilindungi relatif setara. Batch export dan metadata lookup dapat berbeda beberapa orde magnitudo dalam CPU time, database work, atau byte yang ditransfer.

Weighted cost membuat bucket lebih dekat dengan perkiraan resource demand.

metadata lookup:  1 token
search request:   3 token
batch export:    20 token

Request dengan cost 20 token tidak dapat lolos ketika saldo hanya 12, meski jumlah request pada detik tersebut masih rendah. Limiter dengan demikian merespons work class yang dikonfigurasi, bukan sekadar raw request count.

Weight tetap merupakan pendekatan. Jika actual cost sangat bervariasi di dalam satu class, static weight masih dapat menerima kombinasi pekerjaan yang mahal. Pengukuran downstream saturation dan completion latency tetap diperlukan.

Scope menentukan traffic yang berbagi budget

Token bucket tidak memiliki fairness semantics yang berguna sebelum key-nya ditentukan. Satu global bucket melindungi kapasitas agregat, tetapi satu caller yang sibuk dapat menghabiskan seluruh burst budget. Bucket per-user mengisolasi user, tetapi aggregate traffic dapat melewati limit backend bersama.

Scope yang umum antara lain:

global
tenant:{tenant_id}
user:{user_id}
api_key:{key_id}
route:{route_id}
tenant:{tenant_id}:route:{route_id}

Sistem sering menggabungkan beberapa scope. Sebuah request dapat memerlukan credit dari global bucket sekaligus tenant bucket. Global bucket melindungi shared infrastructure, sedangkan tenant bucket membatasi porsi satu tenant.

Admission pada beberapa bucket memerlukan semantic atomic untuk set yang diwajibkan. Mengurangi satu bucket lalu gagal pada bucket lain dapat membocorkan credit kecuali implementasi dapat melakukan rollback dengan aman atau menjalankan keputusan secara atomic.

Distributed bucket menukar presisi dengan biaya koordinasi

Limiter pada satu process dapat memperbarui local bucket di bawah lock atau atomic operation. Service dengan banyak instance menghadapi pilihan yang lebih rumit. Bucket independen per-instance melipatgandakan effective burst budget dan refill rate kecuali nilai konfigurasi dibagi antar-instance.

Shared bucket dengan koordinasi kuat dapat menerapkan global bound yang lebih ketat, tetapi setiap admission decision dapat menambah network dan storage contention. Jalur koordinasi tersebut dapat menjadi bottleneck tersendiri.

Desain praktis menetapkan precision boundary secara eksplisit. Pilihannya mencakup membagi global budget ke beberapa instance, menyewakan kelompok token ke local limiter, atau memakai centralized atomic store untuk traffic yang membutuhkan strict global enforcement dan layak membayar biaya koordinasinya.

Token leasing dapat mengurangi koordinasi per-request:

global pool
    |
    +-- lease 50 token --> instance A
    +-- lease 50 token --> instance B

Tradeoff-nya adalah imprecision sementara. Token yang disewakan ke instance idle atau gagal dapat tidak tersedia sampai lease berakhir atau direklamasi. Lease size menentukan keseimbangan antara frekuensi koordinasi dan stranded capacity.

Retry response tidak boleh membentuk gelombang serempak

Request yang ditolak sering memicu retry dari client. Mengembalikan overload response tanpa retry policy dapat memindahkan tekanan dari service ke tight retry loop.

Untuk HTTP API, 429 Too Many Requests lazim dipakai untuk policy-based rate limiting. Retry-After dapat menyampaikan delay ketika service memiliki estimasi yang bermakna. Client tetap perlu memakai retry terbatas dan jitter agar banyak caller tidak kembali pada saat yang sama.

Telemetry limiter sebaiknya memisahkan original operation dari retry attempt. Tanpa pemisahan tersebut, retry wave dapat terlihat seperti pertumbuhan traffic organik.

Field yang berguna antara lain:

limiter=tenant_api
result=rejected
bucket_capacity=100
refill_per_second=20
token_cost=3
tokens_remaining=1.4

Konfigurasi menentukan proteksi sekaligus pengalaman caller

Refill rate di bawah legitimate demand normal menghasilkan rejection terus-menerus. Bucket yang jauh di atas safe transient capacity mengizinkan burst yang cukup besar untuk memindahkan bottleneck ke downstream. Karena itu, kedua parameter memerlukan dasar resource yang jelas.

Service yang dilindungi mungkin mampu menangani 500 request per detik pada steady state dan queue singkat berisi 200 request tambahan. Kondisi itu tidak otomatis berarti r = 500 dan B = 200; request cost, concurrent execution, downstream limit, dan queue yang sudah ada ikut menentukan konfigurasi yang aman. Load test dan production telemetry memberi evidence untuk menetapkan nilainya.

Rate limiting juga memerlukan observability pada scope yang sama dengan enforcement. Aggregate rejection metric dapat menyembunyikan satu tenant yang menghabiskan bucket sementara tenant lain tidak terdampak.

Token bucket paling berguna ketika dua dimensinya tetap eksplisit: replenishment mengendalikan sustained admission, sedangkan finite stored credit mengendalikan burst size. Pemisahan peran tersebut membuat overload policy lebih dapat diprediksi tanpa memaksa traffic yang secara alami bursty masuk ke batas per-detik yang terlalu kaku.