limit_req untuk wp-login.php itu kelihatannya gampang. Tulis beberapa baris di konfigurasi Nginx, nginx -t lolos, reload, selesai. Masalahnya, nginx -t hanya memastikan sintaksnya benar. Ia tidak memastikan aturan itu benar-benar kena ke request yang datang, jadi konfigurasi yang terlihat rapi bisa saja tidak pernah menolak satu request pun. Di bawah ini beberapa pertanyaan yang biasanya muncul saat kita ingin membuktikan rate limiting (pembatasan jumlah request per satuan waktu) itu memang jalan, plus cara mengeceknya sendiri.
Kenapa limit_req sudah dipasang tapi sepertinya tidak berpengaruh?
location. Untuk setiap request, Nginx hanya memilih satu blok location. Kalau kamu menulis location /wp-login.php lalu menaruh limit_req di dalamnya, itu prefix match biasa. Nginx tetap mengecek location regex seperti location ~ \.php$, dan kalau regex cocok, regex yang menang. Hasilnya, request ke wp-login.php diproses di blok PHP umum yang tidak punya limit_req sama sekali. Supaya pasti menang, pakai exact match location = /wp-login.php.
Konsekuensinya, blok exact match itu harus bisa menjalankan PHP sendiri. Isinya perlu fastcgi_pass dan include parameter fastcgi yang sama dengan blok PHP utama, karena Nginx tidak meneruskan request ke location lain setelah satu location terpilih. Kalau bagian ini lupa, gejalanya bukan lagi "limit tidak jalan", tapi halaman login error atau file PHP malah terunduh.
Penyebab lain biasanya lebih sederhana. Konfigurasi belum di-reload. File yang kamu edit ternyata tidak di-include, dan ini cukup umum di server dengan control panel yang membangkitkan ulang file config. Atau yang memegang port 80/443 sebenarnya bukan Nginx. Kalau Nginx berdiri sebagai reverse proxy di depan Apache atau LiteSpeed, limit_req tetap berlaku asal ditaruh di location yang mem-proxy request tersebut.
Bagaimana cara mengujinya sendiri dengan curl?
BASH
# 1. Pastikan aturan ada di konfigurasi yang sedang dimuat
sudo nginx -T 2>/dev/null | grep -n 'limit_req'
# 2. Kirim 30 request, 10 sekaligus, lalu hitung status code-nya
seq 1 30 | xargs -P 10 -I{} curl -s -o /dev/null -w '%{http_code}
' https://example.com/wp-login.php | sort | uniq -c
# 3. Cocokkan dengan error log (path tergantung setup)
sudo grep 'limiting requests' /var/log/nginx/error.log | tail -n 20nginx -T, yang mencetak seluruh konfigurasi hasil gabungan semua include. Kalau limit_req tidak muncul di output-nya, berarti file yang kamu edit memang tidak dimuat. Perintah kedua cukup memakai GET biasa ke halaman login, tanpa perlu mengirim username dan password. Ganti example.com dengan domainmu, lalu jalankan dari mesin di luar server (laptop atau VPS lain) supaya jalurnya sama dengan jalur pengunjung.
Sebagai contoh ilustratif, dengan rate=1r/s burst=5 nodelay kamu mungkin melihat sekitar enam respons 200 dan sisanya 429 atau 503. Angka persisnya tergantung timing dan latency. Yang penting, penolakan itu ada. Kalau ketiga puluh request-nya 200 semua, ada yang belum beres.
Satu peringatan sebelum menjalankan tes ini. Cek dulu apakah server memakai fail2ban dengan jail yang membaca log limit_req, misalnya filter nginx-limit-req. Kalau jail itu aktif, IP-mu bisa ikut diblokir. Siapkan akses cadangan ke server, atau whitelist IP-mu sementara di fail2ban.
Kenapa yang muncul 503, bukan 429? Apakah itu tanda berhasil?
limit_req_status 429;. Keduanya sama-sama bisa berarti limit bekerja. Bedanya, 503 juga bisa datang dari sumber lain, misalnya mode maintenance WordPress atau backend yang kewalahan di beberapa setup. Jadi melihat 503 saja belum membuktikan apa-apa.
Karena itu aku lebih suka mengganti statusnya ke 429. Di access log maknanya langsung jelas, monitoring tidak salah membaca rate limit sebagai server down, dan saat customer mengirim screenshot error, tim support bisa langsung tahu itu pembatasan request, bukan gangguan server.
Ada juga kasus yang sering bikin bingung: tidak ada 429 atau 503 sama sekali, tapi sebagian request terasa lambat. Ini biasanya terjadi karena burst dipasang tanpa nodelay. Tanpa nodelay, request yang masuk ke jatah burst tidak ditolak, melainkan diantrekan lalu dilepas sesuai rate. Curl tetap menerima 200, hanya lebih lama. Penolakan baru muncul setelah antrean burst penuh. Kalau ingin melihat efek antrean ini, tambahkan %{time_total} ke opsi -w di curl.
Bagaimana memastikan lewat error log bahwa yang menolak memang limit_req?
error. Contoh format barisnya kira-kira seperti ini: limiting requests, excess: 5.120 by zone "wplogin", client: 203.0.113.10, server: example.com, request: "GET /wp-login.php HTTP/1.1". Nama zone ikut tercatat, jadi kamu bisa tahu aturan mana yang kena. Kalau curl menerima 503 tapi baris seperti ini tidak ada sama sekali, penolakan itu hampir pasti datang dari tempat lain.
Request yang ditunda (kasus tanpa nodelay) dicatat sebagai delaying request, satu level di bawah limit_req_log_level. Dengan nilai default error, baris delay masuk di level warn. Kalau error_log di server-mu hanya mencatat level error ke atas, baris delay tidak akan pernah muncul, meskipun antreannya jalan.
Perhatikan juga file log mana yang kamu baca. Kalau virtual host punya direktif error_log sendiri, baris ini masuk ke file tersebut, bukan ke log global. Path-nya pun berbeda antar distro dan control panel. Kalau kamu ingin menguji aturan baru di production tanpa menolak siapa pun, versi Nginx yang lebih baru punya limit_req_dry_run on;. Dengan opsi ini, Nginx hanya mencatat request yang seharusnya ditolak, tanpa benar-benar menolaknya. Cek dulu apakah versimu mendukungnya.
Kenapa hasil tes berbeda kalau situs di belakang Cloudflare atau reverse proxy?
$binary_remote_addr berisi IP yang terhubung langsung ke Nginx. Kalau di depannya ada CDN atau proxy, yang tercatat adalah IP proxy, bukan IP pengunjung. Dampaknya bisa dua arah. Pengunjung yang lewat node proxy yang sama berbagi satu jatah, sehingga user asli ikut kena 429 saat bot menyerang. Sebaliknya, request dari satu bot bisa tersebar ke banyak node proxy, sehingga batas per IP jarang tercapai. Cara mengeceknya mudah: lihat nilai client: di baris limiting requests. Kalau isinya IP proxy, berarti Nginx belum membaca IP asli.
Solusinya adalah modul realip. Pakai set_real_ip_from untuk rentang IP proxy yang kamu percaya, dan real_ip_header sesuai header yang dikirim proxy, misalnya CF-Connecting-IP untuk Cloudflare atau X-Forwarded-For untuk proxy lain. Ambil daftar rentang IP dari dokumentasi resmi provider. Jangan pernah mempercayai header itu dari semua IP, karena siapa pun bisa memalsukan header dan dengan mudah lolos dari limit.
Kemungkinan terakhir, kamu menguji dari IP yang dikecualikan. Pola whitelist yang umum memakai geo atau map supaya IP kantor menghasilkan key zone kosong, dan Nginx tidak menghitung request yang key-nya kosong. Kalau tes dijalankan dari IP itu, hasilnya akan selalu 200. Jalankan tes dari jaringan lain supaya hasilnya mewakili pengunjung biasa.
Singkatnya, urutan pengecekannya begini. Pastikan aturan termuat lewat nginx -T. Pastikan wp-login.php jatuh ke location yang benar. Uji secara paralel dari luar server. Terakhir, cocokkan status yang kamu terima dengan baris limiting requests di error log. Kalau keempatnya konsisten, kamu tahu limit-nya benar-benar bekerja, bukan sekadar tertulis di file. Endpoint lain yang sering diserang bot butuh perlakuan serupa, dan pembahasan untuk XML-RPC ada di Rate Limiting xmlrpc.php to Prevent Recurring Server Load Spikes.