Home / Artikel / Web Development
Web Development

API Terbuka Bukan Berarti Bebas Diserbu: Cara Menerapkan Rate Limiting di PHP

Rate limiting membantu membatasi seberapa sering sebuah endpoint API boleh dipanggil dalam periode tertentu. Dengan aturan yang tepat, aplikasi lebih tahan terhadap bot, penyalahgunaan, dan lonjakan trafik tanpa langsun…

API Terbuka Bukan Berarti Bebas Diserbu: Cara Menerapkan Rate Limiting di PHP

Sebuah endpoint API bisa terlihat baik-baik saja saat hanya dipakai beberapa pengguna. Masalah biasanya muncul ketika ada bot yang mengulang permintaan, skrip yang salah konfigurasi, atau fitur frontend yang tanpa sengaja memanggil endpoint berkali-kali. Tanpa pembatasan, server PHP dapat terus memproses permintaan yang sebenarnya tidak perlu.

Di sinilah rate limiting berguna. Sederhananya, rate limiting adalah aturan yang membatasi berapa banyak permintaan yang boleh dilakukan oleh satu pengguna, IP address, token, atau aplikasi dalam periode tertentu. Contohnya, satu alamat IP boleh mengakses endpoint pencarian maksimal 60 kali per menit.

Rate limiting bukan pengganti autentikasi dan bukan solusi tunggal untuk serangan besar. Namun, ini adalah lapisan perlindungan yang relatif sederhana dan sering terlupakan ketika sebuah aplikasi mulai menerima trafik dari luar.

Kenapa pembatasan permintaan penting?

Tanpa rate limiting, satu endpoint dapat menjadi titik lemah. Endpoint login bisa dipakai untuk mencoba banyak kombinasi password. Endpoint pencarian dapat dipanggil ribuan kali untuk menguras resource database. Endpoint pengiriman email bisa disalahgunakan untuk mengirim spam.

Bahkan tanpa niat jahat, pengguna normal juga dapat menimbulkan beban besar. Misalnya, JavaScript di halaman melakukan polling setiap detik, lalu dibuka oleh ratusan pengguna. Dari sisi browser, semuanya tampak wajar. Dari sisi server, jumlah query dan koneksi dapat meningkat cepat.

Analisis praktis: rate limiting memaksa kita menentukan nilai sebuah permintaan. Endpoint daftar produk mungkin membutuhkan batas yang cukup longgar, sedangkan endpoint login, reset password, dan pengiriman OTP seharusnya memiliki aturan jauh lebih ketat.

Tentukan apa yang ingin dibatasi

Kesalahan umum adalah memakai satu angka untuk semua endpoint. Padahal, kebutuhan setiap fitur berbeda. Sebelum menulis kode, tentukan tiga hal berikut:

  • Identitas pembatas: apakah berdasarkan IP, user ID, API key, session, atau gabungan beberapa identitas?
  • Jendela waktu: apakah batas dihitung per detik, menit, jam, atau hari?
  • Respons saat batas tercapai: apakah server mengembalikan HTTP 429, meminta pengguna menunggu, atau menerapkan antrean?

Untuk endpoint publik, IP address sering menjadi titik awal. Untuk pengguna yang sudah login, user ID atau API key biasanya lebih akurat. Jika hanya memakai IP, beberapa pengguna dalam satu jaringan kantor atau jaringan seluler bisa ikut terkena batas yang sama.

Respons yang benar saat limit terlampaui

Ketika permintaan ditolak karena terlalu sering, gunakan status HTTP 429 Too Many Requests. Status ini memberi sinyal yang jelas kepada client bahwa masalahnya bukan endpoint tidak ditemukan atau server mengalami error umum.

Respons juga dapat menyertakan informasi sederhana seperti kapan client boleh mencoba lagi. Header Retry-After berguna untuk memberi waktu tunggu dalam detik.

HTTP/1.1 429 Too Many Requests
Retry-After: 30
Content-Type: application/json

Di sisi JSON, formatnya bisa dibuat konsisten:

{
  "error": "rate_limit_exceeded",
  "message": "Terlalu banyak permintaan. Coba lagi beberapa saat lagi.",
  "retry_after": 30
}

Pesan untuk pengguna boleh ramah, tetapi jangan membocorkan detail internal seperti struktur penyimpanan counter atau aturan keamanan yang terlalu spesifik.

Contoh sederhana dengan PHP dan Redis

Untuk aplikasi yang berjalan pada satu server, counter sederhana mungkin bisa disimpan di memori lokal. Namun, pendekatan itu menjadi bermasalah ketika aplikasi berjalan di beberapa server. Setiap server dapat memiliki catatan limit sendiri sehingga pembatasan tidak konsisten.

Redis sering digunakan untuk menyimpan counter yang memiliki masa berlaku. Contoh berikut memakai konsep fixed window: setiap IP memiliki jatah permintaan dalam satu menit.

<?php

$key = 'rate_limit:' . ($_SERVER['REMOTE_ADDR'] ?? 'unknown');
$limit = 60;
$window = 60;

$current = $redis->incr($key);

if ($current === 1) {
    $redis->expire($key, $window);
}

if ($current > $limit) {
    http_response_code(429);
    header('Retry-After: ' . $window);
    echo json_encode([
        'error' => 'rate_limit_exceeded',
        'message' => 'Terlalu banyak permintaan.'
    ]);
    exit;
}

Contoh tersebut masih sederhana. Pada implementasi produksi, perhatikan operasi yang bersifat atomik, cara menentukan durasi tunggu yang sebenarnya, pencatatan log, dan validasi alamat IP jika aplikasi berada di belakang reverse proxy.

Fixed window, sliding window, atau token bucket?

Fixed window paling mudah dipahami: misalnya 60 permintaan pada menit 12:00:00 sampai 12:00:59. Kekurangannya, pengguna dapat mengirim banyak permintaan di ujung satu jendela dan awal jendela berikutnya.

Sliding window menghitung permintaan dalam rentang waktu yang terus bergerak. Hasilnya lebih halus, tetapi penyimpanannya bisa lebih kompleks karena sistem perlu melacak waktu setiap permintaan atau membuat agregasi yang lebih rinci.

Token bucket bekerja seperti ember berisi token. Setiap permintaan mengambil satu token, sementara token baru ditambahkan secara berkala sampai batas tertentu. Model ini memungkinkan lonjakan kecil yang masih wajar, tetapi tetap menjaga rata-rata trafik.

Untuk banyak aplikasi kecil, fixed window sudah cukup sebagai langkah awal. Jangan membuat algoritma terlalu rumit sebelum memahami pola trafik yang sebenarnya.

Jangan lupa membatasi endpoint yang sensitif

Beberapa endpoint layak diberi batas khusus:

  • Login dan pemeriksaan password.
  • Permintaan kode OTP atau reset password.
  • Pengiriman email, notifikasi, dan formulir kontak.
  • Pencarian yang menjalankan query berat.
  • Ekspor data atau pembuatan laporan.
  • Endpoint yang memanggil layanan pihak ketiga berbayar.

Untuk login, pembatasan berdasarkan IP saja belum cukup. Tambahkan juga pembatasan berdasarkan identitas akun atau kombinasi IP dan username yang dinormalisasi. Dengan begitu, penyerang tidak mudah menguji banyak password terhadap satu akun, tetapi pengguna dalam jaringan bersama tidak langsung diblokir seluruhnya.

Apa artinya bagi kita?

Rate limiting sebaiknya diperlakukan sebagai bagian dari desain API, bukan tambalan setelah server bermasalah. Setiap endpoint publik perlu memiliki ekspektasi penggunaan: berapa banyak permintaan normal, apa yang dianggap tidak wajar, dan bagaimana client seharusnya bereaksi.

Yang bisa dilakukan sekarang adalah mulai dari endpoint paling berisiko. Catat jumlah permintaan, ukur penggunaan normal selama beberapa hari, lalu tetapkan batas dengan ruang yang cukup. Tambahkan respons 429, dokumentasikan aturan untuk frontend, dan pastikan client tidak melakukan retry tanpa jeda.

Terakhir, pantau metriknya. Jika banyak pengguna normal terkena limit, atur ulang kebijakan. Jika satu endpoint sering menerima permintaan berulang dari sumber yang sama, periksa apakah penyebabnya bug frontend, integrasi yang salah, atau pola penyalahgunaan.

– Rio Yotto @rioyotto