Melindungi API dari brute-force dan denial-of-service: express-rate-limit dengan store Redis untuk distribusi, express-slow-down untuk throttling, proteksi endpoint login, dan strategi lapisan pertahanan.

Setelah di episode 19 kalian mengelola kerentanan dependency, episode 20 membahas serangan yang menyerang ketersediaan: brute-force dan denial-of-service. Autentikasi yang kuat (episode 12) tidak ada artinya jika penyerang bisa mencoba jutaan password, dan server yang sehat tidak berguna jika bisa dibanjiri request.
Mengapa rate limiting ada di lapisan aplikasi, bukan hanya di firewall/CDN? Karena kontrol terbaik adalah yang dekat dengan logika: rate limit per akun hanya bisa dipahami aplikasi, bukan infrastruktur. Di episode ini kalian membangun dua lapisan — express-rate-limit untuk batas tegas dan express-slow-down untuk pelambatan halus.
npm install express-rate-limitimport { 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" },
})Middleware ini menghitung request per IP dalam jendela 60 detik; request ke-101 ditolak dengan status 429. standardHeaders: "draft-7" mengirim header RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset yang bisa dibaca client.
Batas global melindungi keseluruhan; route sensitif butuh batas lebih ketat:
export const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
limit: 20,
standardHeaders: "draft-7",
legacyHeaders: false,
message: { error: "TOO_MANY_ATTEMPTS", message: "Terlalu banyak percobaan login" },
})import { authLimiter } from "../middleware/rateLimit.js"
router.post("/auth/login", authLimiter, login)20 percobaan login per 15 menit per IP memperlambat brute-force drastis — coba password bisa jutaan per jam hanya jika tidak dibatasi. Batas ini menutup endpoint yang paling sering diserang.
Tip
Untuk brute-force yang lebih presisi, batasi per akun, bukan hanya per IP — IP bisa diganti penyerang (banyak proxy), akun sulit. Kombinasikan: rate limit ketat per akun + rate limit longgar per IP sebagai jaring pengaman. Ini pola yang sama dengan pembahasan akun vs IP di series autentikasi.
Rate limiter default menyimpan counter di memori proses — satu instance masing-masing punya hitungan sendiri. Saat aplikasi diskala ke banyak instance (episode 25), penyerang bisa membagi request ke banyak instance dan melewati batas. Solusinya: store bersama di Redis:
npm install @rate-limit/redis ioredisimport { RedisStore } from "@rate-limit/redis"
import { Redis } from "ioredis"
import { rateLimit } from "express-rate-limit"
import { config } from "../config.js"
const client = new Redis(config.redisUrl)
export const globalLimiter = rateLimit({
windowMs: 60 * 1000,
limit: 100,
standardHeaders: "draft-7",
legacyHeaders: false,
store: new RedisStore({ sendCommand: (...args) => client.sendCommand(args) }),
})Sekarang hitungan rate limit dibagikan antar instance — satu counter global di Redis, tidak peduli instance mana yang melayani request. Tanpa store bersama, scaling justru melemahkan proteksi.
Kadang menolak (429) bukan perilaku terbaik — user sah yang sedikit melampaui batas akan frustrasi. express-slow-down memperlambat respons alih-alih menolak:
npm install express-slow-downimport { 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 dalam satu menit, setiap request berikutnya ditunda — semakin banyak hits, semakin lama delay (hingga 1 detik). Penyerang otomatis melambat; user manusia tidak merasakan apa-apa. Kombinasi ideal: slowDown untuk seluruh API, rateLimit ketat untuk login dan aksi mahal.
Rate limit tidak menolong saat satu request membawa body 2 GB. Batasi ukuran sejak parser (episode 5):
app.use(express.json({ limit: "1mb" }))
app.use(express.urlencoded({ extended: true, limit: "1mb" }))multer({ limits: { fileSize: 5 * 1024 * 1024 } })Aksi yang butuh komputasi berat sebaiknya tidak memblokir thread utama — dipindah ke background job.
Kontrol di level edge (CDN/WAF) menahan volume besar; lapisan aplikasi menangkap pola yang butuh konteks bisnis. Keduanya bekerja bersama — tidak ada satu kontrol yang menangani semuanya.
Tanpa store Redis, setiap instance punya hitungan sendiri — scaling mengalikan batas efektif. Pasang store bersama sebelum scaling.
Client bingung saat menerima 429 kosong. Kirim bentuk { error, message } (konsisten dengan episode 7) dan header Retry-After agar client tahu kapan boleh mencoba lagi.
Tanpa trust proxy, semua request terlihat berasal dari IP proxy/load balancer — satu IP yang sama, sehingga semua user kena limit. Atur app.set("trust proxy", 1) saat di belakang proxy (episode 25).
Warning
Batas yang terlalu rendah pada route umum bisa mengunci user sah (misal NAT satu kantor berbagi IP). Pilih nilai berdasarkan data nyata — ukur traffic normal dengan logging episode 17, lalu tetapkan batas dengan margin. Rate limiting melindungi; rate limiting yang salah justru membuat aplikasi kalian ditolak pengguna sendiri.
Episode 20 menambah lapisan ketersediaan: express-rate-limit dengan Redis store untuk batas global dan per-route, express-slow-down untuk pelambatan halus, pembatasan ukuran body/upload, dan peta lapisan pertahanan dari CDN hingga handler.
Inti yang harus dibawa pulang:
express-rate-limit menolak dengan 429; pasang batas ketat di login, longgar di route umum.express-slow-down menunda alih-alih menolak — lebih ramah user sah.limit: "1mb") dan upload (fileSize).trust proxy agar rate limit membaca IP client yang benar.Di episode 21 selanjutnya kita akan mengoptimalkan performansi: caching & performance — Redis cache, HTTP caching headers, ETag, dan gzip/compression. Sampai jumpa di episode 21!