Rate Limiting #
Ketika aplikasi kita go-live di internet, aplikasi tersebut akan langsung terekspos ke berbagai jenis bot, web scraper otomatis, hingga percobaan peretasan massal (brute-force attacks). Salah satu skenario yang sering melumpuhkan server adalah ketika penyerang membanjiri endpoint yang memakan banyak daya CPU dan database (seperti halaman pencarian produk, endpoint autentikasi login, atau pencetakan invoice PDF) dengan ribuan request dalam hitungan detik. Tanpa perlindungan, server aplikasi kita akan cepat kehabisan sumber daya dan mati (crash).
Untuk melindungi infrastruktur web server dari penyalahgunaan trafik tersebut, kita wajib menerapkan Rate Limiting (Pembatasan Laju Request). Nginx menyediakan fitur rate limiting bawaan yang sangat efisien dan andal menggunakan modul ngx_http_limit_req_module. Di artikel ini, kita akan membedah secara menyeluruh bagaimana algoritma rate limiting bekerja, cara mendefinisikan shared memory zone, penggunaan parameter kritis seperti burst dan nodelay, penyesuaian kode respons HTTP 429 kustom, serta cara membuat pengecualian (whitelist) untuk alamat IP tepercaya.
Cara Kerja: Algoritma Leaky Bucket #
Nginx mengimplementasikan rate limiting menggunakan algoritma Leaky Bucket (Ember Bocor). Mari kita analogikan sistem ini untuk memudahkan pemahaman:
Bayangkan sebuah ember yang memiliki lubang kecil di bagian dasarnya.
- Air yang dituangkan ke dalam ember merepresentasikan request HTTP yang masuk dari browser klien ke server Nginx.
- Tetesan air yang keluar dari lubang dasar merepresentasikan request yang diizinkan lolos oleh Nginx untuk diteruskan ke backend aplikasi dengan laju yang konstan dan stabil.
- Ember itu sendiri bertindak sebagai antrean penyangga (buffer queue) yang kita sebut sebagai
burst. - Air yang meluap dari bibir ember merepresentasikan request berlebih yang langsung ditolak dan dibuang oleh Nginx (dikembalikan sebagai error ke klien).
Berikut adalah alur logika pengambilan keputusan rate limiting di Nginx:
flowchart TD
Req["Request Klien Masuk"] --> CheckZone{"Cek Memori Sesi IP"}
CheckZone -->|Belum Melebihi Rate| Allow["Izinkan Request<br>Diteruskan ke Backend"]
CheckZone -->|Melebihi Rate| CheckBurst{"Apakah Ada Slot Kosong di Antrean (Burst)?"}
CheckBurst -->|Ya, Ada Slot| Queue{"Masukkan ke Antrean<br>(Diproses dengan Delay)"}
CheckBurst -->|Tidak Ada Slot| Reject["Tolak Request Instan<br>(Kembalikan Status 429)"]
Queue -->|Jika Pakai Nodelay| Allow
Queue -->|Tanpa Nodelay| Delay["Tunda Pemrosesan Sesuai Antrean"] --> Allow
classDef allow fill:#10b981,stroke:#059669,color:#ffffff;
classDef reject fill:#ef4444,stroke:#dc2626,color:#ffffff;
class Allow allow;
class Reject reject;Mendefinisikan Zone Rate Limit (limit_req_zone)
#
Sebelum kita menerapkan rate limit pada endpoint tertentu, kita harus mendefinisikan area memori bersama (shared memory zone) untuk melacak riwayat request dari para pengguna. Zone ini wajib didefinisikan di tingkat konteks http (di luar blok server), karena data pelacakan ini harus dibagikan dan diakses secara real-time oleh seluruh worker process Nginx.
Berikut adalah sintaksis pembuatan zone rate limit:
http {
# Format: limit_req_zone [key] zone=[nama_zone]:[ukuran_memori] rate=[laju_request];
# Contoh 1: Batasi per IP klien - Laju 10 request per detik
limit_req_zone $binary_remote_addr zone=limit_per_ip:10m rate=10r/s;
# Contoh 2: Batasi per IP untuk login - Laju 5 request per menit
limit_req_zone $binary_remote_addr zone=limit_login:10m rate=5r/m;
}
Bedah Parameter Direktif limit_req_zone:
#
- Key (
$binary_remote_addr): Variabel Nginx yang digunakan sebagai pengidentifikasi unik siapa yang akan dibatasi.- Mengapa
$binary_remote_addrdan bukan$remote_addr? Variabel$remote_addrmenyimpan alamat IP klien sebagai string teks biasa (misalnya192.168.100.250yang berukuran 15 byte). Sementara$binary_remote_addrmenyimpan alamat IP dalam format biner representasi biner (hanya memakan 4 byte untuk IPv4 dan 16 byte untuk IPv6). Penggunaan biner menghemat memori server secara drastis saat menangani ratusan ribu IP pengunjung.
- Mengapa
- Zone (
zone=limit_per_ip:10m): Menentukan nama zone kustom kita (limit_per_ip) dan kapasitas alokasi shared memory (10m= 10 Megabyte).- Alokasi memori sebesar
1m(1 Megabyte) dapat menyimpan sekitar 16.000 data IP unik beserta statistik waktu aksesnya. Dengan ukuran10m, Nginx dapat melacak sekitar 160.000 alamat IP aktif secara bersamaan. Jika memori penuh, Nginx akan menghapus data IP terlama secara otomatis.
- Alokasi memori sebesar
- Rate (
rate=10r/sataurate=5r/m): Batas laju request yang diizinkan.10r/s(10 request per second): Nginx mengizinkan rata-rata 1 request setiap 100 milidetik.5r/m(5 request per minute): Nginx mengizinkan rata-rata 1 request setiap 12 detik.
Menerapkan Rate Limiting di Location Block (limit_req)
#
Setelah mendefinisikan zone di konteks http, kita dapat mengaktifkan pembatasan tersebut di dalam blok server atau location menggunakan direktif limit_req.
Skenario 1: Pembatasan Ketat Tanpa Toleransi (Tanpa Burst) #
Ini adalah bentuk rate limiting paling dasar, tetapi sering kali terlalu agresif untuk aplikasi web nyata.
server {
listen 443 ssl;
server_name example.com;
location /api/docs/ {
# Gunakan zone yang telah didefinisikan
limit_req zone=limit_per_ip;
proxy_pass http://backend;
}
}
- Perilaku: Jika kita menetapkan
rate=10r/spada zone, Nginx mengharapkan jeda waktu 100ms antar request. Jika browser pengguna mengirimkan 2 request sekaligus (misalnya request HTML utama disusul request file CSS dalam jeda 5ms), request kedua akan langsung ditolak dengan error status 503 oleh Nginx. Browser pengguna akan mendapati banyak aset gagal termuat.
Skenario 2: Menggunakan Antrean Penyangga (burst)
#
Untuk mencegah penolakan aset yang sah akibat pemuatan halaman browser yang naturally mengirim request paralel di awal koneksi, kita harus menyertakan parameter burst:
location /api/ {
# Izinkan antrean penangguhan hingga 20 request
limit_req zone=limit_per_ip burst=20;
proxy_pass http://api_backend;
}
- Perilaku: Jika pengguna mengirimkan 15 request secara instan, 1 request pertama langsung diteruskan ke backend. 14 request berikutnya tidak ditolak, melainkan dimasukkan ke dalam antrean (ember penyangga) berkapasitas 20 slot. Nginx kemudian memproses 14 request dalam antrean tersebut secara berurutan dengan jeda masing-masing 100ms.
- Kelemahan: Pengguna akan merasakan bahwa website mereka memuat sangat lambat (delay), karena request mereka tertahan di antrean dan dirilis secara perlahan oleh Nginx.
Skenario 3: Memproses Instan Tanpa Penundaan (burst + nodelay)
#
Untuk API modern atau SPA (Single Page Application), penundaan respons (delay) adalah pengalaman pengguna yang buruk. Solusi terbaik adalah menggabungkan burst dengan parameter nodelay:
location /api/v1/ {
# Proses instan request dalam burst, tapi tetap kunci kuota slot
limit_req zone=limit_per_ip burst=20 nodelay;
proxy_pass http://api_backend;
}
- Perilaku: Jika pengguna mengirimkan 15 request secara instan, seluruh 15 request langsung diproses saat itu juga tanpa penundaan. Namun, 14 slot antrean
burstmilik IP tersebut akan langsung ditandai sebagai “terpakai”. - Kuota slot ini akan dikosongkan kembali secara bertahap sesuai laju
rate=10r/s(1 slot kosong kembali setiap 100ms). Jika pengguna tersebut mengirimkan request tambahan sebelum slot kuota burst mereka kosong kembali, request baru tersebut akan langsung ditolak dengan status error.
Kustomisasi Respons HTTP saat Limit Terlampaui #
Secara default, Nginx mengembalikan kode status HTTP 503 Service Unavailable ketika rate limit pengguna terlampaui. Namun secara semantik standar web, HTTP 503 menandakan server sedang mengalami kendala kelebihan beban secara umum.
Untuk membedakan masalah ini dengan jelas, kita sangat disarankan mengubah kode respons menjadi HTTP 429 Too Many Requests. Klien modern (seperti library HTTP Axios, Fetch, atau bot) mengenali kode 429 dan akan otomatis melakukan jeda coba-ulang (exponential backoff).
Kita dapat mengatur kustomisasi ini menggunakan direktif limit_req_status dan limit_req_log_level:
http {
limit_req_zone $binary_remote_addr zone=limit_per_ip:10m rate=10r/s;
# Ubah kode respons error ke 429
limit_req_status 429;
# Ubah tingkat pencatatan log (default: error, disarankan: warn)
limit_req_log_level warn;
}
Menyisipkan Header Retry-After kustom
#
Praktik keamanan API terbaik adalah memberitahu klien secara spesifik kapan mereka boleh mencoba mengirimkan request kembali melalui header Retry-After:
server {
error_page 429 = @too_many_requests;
location @too_many_requests {
# Beritahu klien untuk menunggu 10 detik
add_header Retry-After 10 always;
add_header Content-Type application/json;
return 429 '{"error": "Terlalu banyak permintaan. Silakan coba beberapa saat lagi.", "retry_after_seconds": 10}';
}
}
Studi Kasus Nyata: Melindungi Halaman Login secara Ekstrem #
Halaman login adalah target empuk serangan kamus (brute-force). Kita harus memperketat laju akses ke endpoint login secara terpisah tanpa mengganggu kenyamanan pengguna saat berselancar di halaman produk umum.
http {
# Zone 1: Rate limit umum aplikasi (30 request per detik)
limit_req_zone $binary_remote_addr zone=app_global:10m rate=30r/s;
# Zone 2: Rate limit khusus login (3 request per menit)
limit_req_zone $binary_remote_addr zone=login_strict:10m rate=3r/m;
}
server {
listen 443 ssl;
server_name example.com;
# Terapkan limit umum untuk seluruh aplikasi
limit_req zone=app_global burst=50 nodelay;
# Terapkan limit ketat khusus untuk endpoint login
location = /auth/login {
# Hanya izinkan 3 kali login per menit, toleransi lonjakan 2 request
limit_req zone=login_strict burst=2 nodelay;
limit_req_status 429;
proxy_pass http://auth_backend;
}
location / {
try_files $uri $uri/ =404;
}
}
Membuat Pengecualian Rate Limit (Whitelist IP) #
Dalam lingkungan produksi, terkadang ada kebutuhan khusus di mana kita harus membebaskan alamat IP tertentu dari aturan rate limiting. Misalnya, IP kantor internal, server monitoring eksternal (seperti Uptime Robot), atau IP API gateway pihak ketiga yang sah.
Kita dapat mengakalinya secara elegan menggunakan kombinasi modul geo dan map:
http {
# Langkah 1: Klasifikasikan IP ke dalam variabel $is_whitelisted
geo $is_whitelisted {
default 1; # Default: kena rate limit (nilai 1)
127.0.0.1 0; # Localhost bebas limit (nilai 0)
10.0.0.0/8 0; # Subnet VPN internal bebas limit
203.0.113.80 0; # IP publik statis kantor bebas limit
}
# Langkah 2: Petakan hasil geo ke key pelacakan Nginx
map $is_whitelisted $limit_key {
0 ""; # Jika bebas limit, kirim string kosong
1 $binary_remote_addr; # Jika kena limit, gunakan IP sebagai key
}
# Langkah 3: Gunakan variabel $limit_key kustom pada zone
limit_req_zone $limit_key zone=dynamic_limit:10m rate=10r/s;
server {
location / {
limit_req zone=dynamic_limit burst=20 nodelay;
proxy_pass http://backend;
}
}
}
Bagaimana Trik Whitelist ini Bekerja? #
Nginx memiliki aturan internal yang sangat penting: jika key pencarian pada direktif limit_req_zone bernilai string kosong (""), Nginx tidak akan melacak request tersebut dan langsung meloloskannya tanpa pembatasan.
- Ketika request datang dari IP kantor
203.0.113.80, modulgeomembaca IP tersebut dan mencocokkannya dengan aturan203.0.113.80 0. Variabel$is_whitelistedbernilai0. - Modul
mapkemudian membaca nilai$is_whitelistedyang bernilai0tersebut dan memetakannya ke variabel$limit_key = "". - Karena
$limit_keybernilai kosong, Nginx tidak memasukkannya ke dalam tabel shared memory zonedynamic_limitdan membiarkan request tersebut langsung lolos tanpa batasan. - Sebaliknya, pengunjung luar (misalnya IP
180.250.2.1) akan terpetakan ke nilai default1olehgeo, sehingga$limit_keyberisi biner IP klien asli. Nginx akan memproses pembatasan secara normal.
Penerapan Banyak Zone Sekaligus (Multiple Rate Limits) #
Nginx mengizinkan kita menerapkan beberapa direktif limit_req sekaligus pada satu cakupan (scope) location. Request yang masuk harus lolos dari seluruh filter zone yang terpasang agar bisa diteruskan ke backend. Jika salah satu zone saja membatasi request, akses akan langsung ditolak.
Teknik ini sangat berguna untuk menerapkan pertahanan berlapis: membatasi laju request per individu IP secara ketat, sekaligus melindungi server backend secara kolektif dengan membatasi total kapasitas request yang bisa ditangani oleh seluruh server block.
http {
# Zone A: Membatasi IP individu secara ketat (10 request per detik per IP)
limit_req_zone $binary_remote_addr zone=per_client:10m rate=10r/s;
# Zone B: Membatasi total request ke seluruh server (500 request per detik untuk semua user)
limit_req_zone $server_name zone=global_server:10m rate=500r/s;
}
server {
listen 443 ssl;
server_name api.example.com;
location / {
# Klien individu dibatasi burst 20
limit_req zone=per_client burst=20 nodelay;
# Server secara keseluruhan dibatasi burst 100
limit_req zone=global_server burst=100;
proxy_pass http://api_backend;
}
}
Dengan konfigurasi di atas:
- Seorang user nakal yang mencoba mengirimkan 50 request/detik sendirian akan langsung dihadang oleh filter
per_clientdan diblokir dengan status 429. - Jika ada lonjakan trafik sah secara masif dari ribuan pengguna berbeda secara bersamaan, filter
per_clientmasing-masing aman. Namun jika total request melebihi kapasitas server (global_server500r/s + burst 100), kelebihannya akan mulai ditolak atau ditunda demi menyelamatkan backend database dari kelumpuhan sistem.
Menganalisis dan Memantau Log Rate Limit #
Melakukan rate limiting di server produksi memerlukan pemantauan berkala. Jika aturan kita terlalu longgar, server tetap berisiko kewalahan. Sebaliknya jika terlalu ketat, pengguna sah akan sering terganggu oleh error status 429.
Memahami Format Log Error Nginx #
Ketika request diblokir atau ditunda oleh rate limiting, Nginx menuliskan pesan kesalahan ke dalam berkas log kesalahan server (error.log). Contoh baris log blokir terlihat seperti ini:
2026/06/16 13:00:00 [warn] 1234#0: *5678 limiting requests, excess: 10.050 by zone "limit_per_ip", client: 198.51.100.12, server: example.com, request: "POST /auth/login HTTP/2.0", host: "example.com"
limiting requests: Menunjukkan request diblokir atau ditunda.excess: Jumlah request yang melampaui laju batas yang ditentukan.zone: Nama shared memory zone yang memicu pemblokiran (limit_per_ip).client: Alamat IP penyerang atau pengguna yang terkena limit (198.51.100.12).request: HTTP Request spesifik yang dilakukan.
Menganalisis Log via Command Line #
Kita dapat menggunakan utilitas CLI standard Linux untuk menganalisis berkas log dan mencari tahu siapa yang paling sering terblokir:
# 1. Cari tahu IP mana yang paling sering terkena rate limit
grep "limiting requests" /var/log/nginx/error.log | awk -F', client: ' '{print $2}' | awk '{print $1}' | sort | uniq -c | sort -nr | head -10
# 2. Cari tahu endpoint mana yang paling sering memicu rate limit
grep "limiting requests" /var/log/nginx/error.log | awk -F', request: ' '{print $2}' | awk -F'"' '{print $1}' | sort | uniq -c | sort -nr | head -10
Dengan memantau metrik ini secara berkala, kita dapat menyesuaikan nilai rate and burst di konfigurasi Nginx agar selaras dengan pola trafik nyata aplikasi kita.
Ringkasan #
- Gunakan
$binary_remote_addr: Selalu gunakan variabel format biner ini sebagai key zone pelacakan untuk menghemat konsumsi memori bersama di server.- Gunakan
burst+nodelay: Kombinasi ini sangat ideal untuk aplikasi web modern karena mempercepat rendering halaman tanpa memberikan penundaan respons (delay) yang merusak UX.- Ubah Status Code ke 429: Selalu sertakan direktif
limit_req_status 429;agar klien API dapat mengenkripsi dan menangani masalah kelebihan request dengan cara coba-ulang yang tepat.- Terapkan Multi-Zone: Bedakan batasan rate limit antara endpoint publik ringan (HTML/CSS) dan endpoint sensitif yang boros database (seperti halaman login atau pencarian).
- Manfaatkan Geo + Map: Gunakan pola pemetaan geo-map untuk mengecualikan IP internal atau agen monitoring tepercaya dari pengawasan rate limit secara bersih.