Mempertahankan ketersediaan API dari brute-force dan banjir request: rate limiting per IP dan per akun dengan store Redis, throttling halus, peran WAF dan CDN dalam melawan DDoS, serta lapisan pertahanan berjenjang

Di episode 17 rate limiting disebut sebagai pertahanan dasar. Sekarang kita bedah penuh: rate limiting dan DDoS protection — dua pertahanan yang menjaga API tetap hidup. Endpoint login tanpa batas bisa dibanjiri jutaan percobaan; API tanpa proteksi banjir bisa roboh saat traffic liar datang — baik dari serangan maupun event yang viral.
Mengapa lapisan ini penting? Karena ini masalah ketersediaan — bukan hanya keamanan data. Server yang down karena DDoS kehilangan uang dan kepercayaan. Episode ini membangun rate limiting yang benar (termasuk store Redis untuk distribusi), throttling halus, dan strategi WAF/CDN melawan DDoS.
Rate limiting membatasi jumlah request dalam satu jendela waktu. Di episode 17 kita pakai express-rate-limit global; sekarang kita bedah strateginya.
import { rateLimit } from "express-rate-limit"
export const globalLimiter = rateLimit({
windowMs: 60 * 1000,
limit: 100,
standardHeaders: "draft-7",
legacyHeaders: false,
message: { error: "TOO_MANY_REQUESTS", message: "Terlalu banyak request" },
})Batas harus berbeda per jenis route — jangan satu batas untuk semua:
| Route | Batas | Alasan |
|---|---|---|
| Login/register | 20 per 15 menit per IP | Anti brute-force |
| Checkout/payment | 10 per menit per akun | Anti abuse biaya |
| Baca katalog | 100 per menit per IP | Ramah untuk umum |
| Webhook internal | 5.000 per menit | Partner sah |
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
limit: 20,
standardHeaders: "draft-7",
legacyHeaders: false,
message: { error: "TOO_MANY_ATTEMPTS", message: "Terlalu banyak percobaan" },
})
router.post("/v1/auth/login", authLimiter, login)Rate limit per IP bisa diakali — penyerang punya ribuan IP (botnet, proxy). Batas per akun jauh lebih efektif karena akun sulit diganti massal:
import { Redis } from "ioredis"
const redis = new Redis(process.env.REDIS_URL!)
async function hitLimit(key: string, max: number, windowSec: number) {
const count = await redis.incr(key)
if (count === 1) await redis.expire(key, windowSec)
return count > max
}
router.post("/v1/auth/login", async (req, res) => {
const email = normalize(req.body.email)
// batas per akun (ketat) + per IP (longgar) = dua jaring pengaman
if (await hitLimit(`login:account:${email}`, 5, 900)) {
return res.status(429).json({ error: "TOO_MANY_ATTEMPTS" })
}
if (await hitLimit(`login:ip:${req.ip}`, 20, 900)) {
return res.status(429).json({ error: "TOO_MANY_ATTEMPTS" })
}
// lanjut validasi credential...
})INCR adalah operasi atomik (episode 6) — aman dari race condition. Kombinasi per akun + per IP adalah pola standar.
Note
Store Redis adalah wajib saat aplikasi diskala (episode 21). Rate limiter di memori proses berarti setiap instance punya hitungan sendiri — dengan 3 instance, batas efektif jadi 3x lipat, dan penyerang tinggal menyebar request. Store bersama di Redis membuat counter global konsisten.
Menolak dengan 429 bisa memukul user sah (NAT kantor berbagi IP). Alternatifnya slow down — memperlambat respons bertahap:
import { slowDown } from "express-slow-down"
export const apiSlowDown = slowDown({
windowMs: 60 * 1000,
delayAfter: 30,
delayMs: (hits) => Math.min((hits - 30) * 100, 1000),
})Setelah 30 request/menit, tiap request berikutnya ditunda hingga 1 detik. Penyerang otomatis melambat; user manusia tidak merasakan. Strategi gabungan: slowDown untuk seluruh API, rateLimit ketat untuk login dan aksi mahal.
DDoS (Distributed Denial of Service) membanjiri sistem dengan traffic dari banyak sumber. Rate limit aplikasi tidak cukup — banjir jutaan paket harus dihentikan di lapisan terluar:
WAF rules contoh:
- Tolak request dengan payload injection pattern
- Challenge browser untuk traffic tanpa header browser
- Blokir IP/ASN yang dikenal sebagai sumber seranganLoad balancer membagi traffic dan bisa menerapkan batas kasar per IP. Ia juga memisahkan internet publik dari service internal — database dan worker tidak pernah terekspos.
Di sini rate limit presisi bekerja: per akun, per route, dengan konteks bisnis yang tidak bisa diketahui WAF.
Warning
DDoS tidak bisa dimenangi sendirian — melawan ratusan Gbps traffic tanpa CDN/WAF mustahil. Strategi yang benar: pindahkan pertahanan ke edge (CDN + WAF) sejak awal, dan pastikan lapisan aplikasi sanggup melayani traffic sah maksimal. Menambah server saat DDoS berlangsung hanya menambah target.
Endpoint yang menimbulkan biaya atau efek samping butuh proteksi ekstra:
const checkoutLimiter = rateLimit({
windowMs: 60 * 1000,
limit: 10,
standardHeaders: "draft-7",
legacyHeaders: false,
})
router.post("/v1/orders/checkout", checkoutLimiter, checkout)Satu request dengan body 2 GB bisa melumpuhkan. Batasi di parser:
app.use(express.json({ limit: "1mb" }))
app.use(express.urlencoded({ extended: true, limit: "1mb" }))Untuk login, tambahkan CAPTCHA atau challenge setelah beberapa percobaan gagal — menghentikan brute-force otomatis tanpa menghukum user.
Rate limiter modern mengirim header yang berguna bagi client:
RateLimit-Limit: 100
RateLimit-Remaining: 95
RateLimit-Reset: 1784149200
Retry-After: 30 # saat kena 429Client yang paham header ini bisa menyesuaikan diri (backoff) alih-alih terus memukul dan kena 429 — mengurangi beban server.
Satu kesalahan paling umum: rate limit membaca IP yang salah karena aplikasi di belakang proxy/load balancer — semua request terlihat dari IP yang sama, dan semua user kena limit bersama.
app.set("trust proxy", 1)Tanpa ini, satu IP proxy mewakili semua user → 429 massal. trust proxy harus diatur saat aplikasi di belakang proxy apa pun (episode 13, 16, 21).
Tip
Ukur lalu tetapkan batas — jangan menebak. Amati traffic normal (episode 11) dan beri margin: jika user sah paling rajin 15 request/menit, batas 50 request/menit aman tanpa melukai siapa pun. Batas yang terlalu rendah lebih berbahaya daripada tidak ada batas: user sah jadi korban.
Counter di memori per instance → scaling melemahkan proteksi. Pakai Redis store sebelum scaling.
Client bingung tanpa pesan dan tanpa Retry-After. Kirim { error, message } + header retry.
Semua user kena limit karena dianggap satu IP. Atur trust proxy.
WAF tidak tahu konteks bisnis (per akun, per route). Rate limit aplikasi tetap diperlukan.
Kantor dengan NAT berbagi IP bisa dikunci bersama. Perhitungkan pola penggunaan nyata.
Episode 20 mempertahankan ketersediaan: rate limiting per IP dan per akun dengan Redis store, throttling halus untuk user sah, dan strategi DDoS berjenjang dari CDN/WAF, load balancer, hingga rate limit presisi aplikasi.
Inti yang harus dibawa pulang:
slowDown memperlambat alih-alih menolak — lebih ramah user sah.trust proxy dan kembalikan header Retry-After.Di episode 21 selanjutnya kita akan membuat sistem tumbuh: scalability & load balancing — scaling horizontal, desain stateless, dan load balancer. Sampai jumpa di episode 21!