Setiap antarmuka pemrograman aplikasi (Application Programming Interface / API) publik maupun gateway internal korporat pasti berhadapan dengan risiko penyalahgunaan lalu lintas trafik. Risiko ini mencakup serangan penolakan layanan (Denial of Service / DoS), bot scraping data harga dan inventaris yang agresif, percobaan peretasan kata sandi secara berulang (brute-force), hingga skrip integrasi pihak ketiga yang mengalami infinite loop.
Tanpa lapisan kontrol lalu lintas yang ketat pada level API Gateway, lonjakan volume request yang tak terkendali akan menguras thread pemrosesan CPU, membanjiri connection pool database, dan melumpuhkan ketersediaan layanan bagi pengguna sah lainnya.
Mekanisme pertahanan paling fundamental untuk menjaga keandalan infrastruktur backend adalah penerapan Rate Limiting. Namun, merancang sistem pembatasan laju trafik terdistribusi yang mampu memproses ratusan ribu request per detik tanpa menimbulkan overhead latensi memerlukan pemahaman mendalam tentang algoritma dan eksekusi atomik pada Redis.
Arsitektur Rate Limiting Terdistribusi Berbasis Token Bucket & Redis:
Klien (Browser / Mobile / B2B)
│
▼ [Request Masuk: GET /api/v1/orders]
┌──────────────────────────────────────────────┐
│ API Gateway (Kong / Traefik / Node.js) │
│ 1. Ekstrak Identitas (API Key / Client IP) │
│ 2. Kirim Perintah Atomik ke Redis via Lua │
└──────┬───────────────────────────────────────┘
│
▼ [EVAL sha_token_bucket.lua key limit capacity refill_rate]
┌──────────────────────────────────────────────┐
│ Redis In-Memory Data Store │
│ - Kapasitas Ember (Bucket Capacity): 100 │
│ - Laju Pengisian (Refill Rate): 10 token/dtk│
│ - Token Tersedia: 84 │
└──────┬───────────────────────────────────────┘
│
├── Jika Token > 0: Kurangi 1 token ──> [Teruskan Request ke Service Backend]
│
└── Jika Token = 0: TOLAK SEGERA ────> [HTTP 429 Too Many Requests]
Header: Retry-After: 5
Membandingkan Kelemahan Fixed Window vs Keunggulan Token Bucket #
Terdapat beberapa pendekatan algoritma yang umum digunakan dalam pembatasan laju trafik:
1. Fixed Window Counter (Penghitung Jendela Tetap) #
Pendekatan ini membagi waktu ke dalam blok statis, misalnya 100 request per menit (00.00 - 00.59).
- Kelemahan Fatal: Rentan terhadap fenomena traffic spike di batas jendela. Jika seorang pengguna mengirimkan 100 request pada detik 00.59 dan 100 request lagi pada detik 01.01, server menerima 200 request hanya dalam kurun waktu 2 detik! Hal ini dapat menumbangkan database seketika.
2. Sliding Window Log (Log Jendela Geser) #
Mencatat timestamp setiap request yang masuk ke dalam sorted set Redis.
- Kelemahan Fatal: Memakan kapasitas memori RAM yang sangat besar jika trafik mencapai jutaan request, karena setiap entri request harus disimpan secara individual.
3. Token Bucket (Ember Token) #
Algoritma ini dianalogikan seperti sebuah ember penampung token dengan kapasitas maksimum tertentu ($C$). Token ditambahkan ke dalam ember secara kontinu dengan laju tetap ($r$ token per detik).
- Setiap kali request masuk, request tersebut harus mengambil satu token dari ember.
- Jika token tersedia di ember, token diambil dan request diteruskan ke backend.
- Jika ember kosong, request langsung ditolak dengan status HTTP 429 Too Many Requests.
- Keunggulan: Mampu mengakomodasi lonjakan trafik mendadak (burst traffic) selama isi token di dalam ember masih mencukupi, sambil tetap menjamin laju konsumsi rata-rata jangka panjang tidak melampaui batas yang ditentukan.
Mengatasi Race Condition dengan Skrip Redis Lua Atomik #
Pada arsitektur cloud modern di mana API Gateway berjalan sebagai kluster multi-kontainer, implementasi rate limiting tidak boleh dilakukan dengan membaca nilai token (GET) lalu memperbaruinya (SET) secara terpisah. Di antara jeda dua operasi jaringan tersebut, ratusan kontainer lain dapat membaca nilai yang sama secara bersamaan (race condition), mengakibatkan jumlah request yang lolos melebihi batas kuota.
Solusi wajib untuk masalah ini adalah mengeksekusi seluruh logika pengecekan dan pengurangan token di dalam satu Skrip Redis Lua yang dijamin dieksekusi secara atomik di dalam single-thread engine Redis.
Berikut adalah implementasi skrip Lua Token Bucket berperforma tinggi:
-- token_bucket.lua
local key = KEYS[1]
local capacity = tonumber(ARGV[1]) -- Kapasitas maksimum ember (burst limit)
local refill_rate = tonumber(ARGV[2]) -- Jumlah token bertambah per detik
local requested = tonumber(ARGV[3]) -- Jumlah token yang diminta (biasanya 1)
local now = tonumber(ARGV[4]) -- Waktu saat ini (epoch timestamp detik)
-- Ambil data state ember sebelumnya
local data = redis.call('HMGET', key, 'tokens', 'last_updated')
local tokens = tonumber(data[1])
local last_updated = tonumber(data[2])
if tokens == nil then
-- Inisialisasi awal saat key pertama kali dibuat
tokens = capacity
last_updated = now
else
-- Hitung pertambahan token berdasarkan selisih waktu yang berlalu
local elapsed = math.max(0, now - last_updated)
tokens = math.min(capacity, tokens + (elapsed * refill_rate))
last_updated = now
end
if tokens >= requested then
-- Token mencukupi: izinkan request dan kurangi token
tokens = tokens - requested
redis.call('HMSET', key, 'tokens', tokens, 'last_updated', last_updated)
-- Set TTL otomatis agar Redis membersihkan kunci yang sudah tidak aktif
redis.call('EXPIRE', key, math.ceil(capacity / refill_rate) * 2)
return { 1, math.floor(tokens) } -- 1 = ALLOWED, tokens tersisa
else
-- Token habis: tolak request
redis.call('HMSET', key, 'tokens', tokens, 'last_updated', last_updated)
return { 0, math.floor(tokens) } -- 0 = BLOCKED
end
Integrasi Middleware API Gateway pada Node.js / Express #
Middleware mengeksekusi skrip Lua di atas dan menyematkan header HTTP standar industri ke browser klien:
// middleware/rateLimiter.ts
import { Request, Response, NextFunction } from 'express';
import Redis from 'ioredis';
import fs from 'fs';
import path from 'path';
const redis = new Redis(process.env.REDIS_URL || 'redis://127.0.0.1:6379');
const luaScript = fs.readFileSync(path.join(__dirname, 'token_bucket.lua'), 'utf8');
// Muat skrip ke Redis SHA script cache untuk eksekusi ultra cepat
let scriptSha: string;
redis.script('load', luaScript).then((sha) => {
scriptSha = sha as string;
});
export async function tokenBucketRateLimiter(
req: Request,
res: Response,
next: NextFunction
) {
// 1. Identifikasi klien: Utamakan API Key, fallback ke IP asli Cloudflare
const clientId = (req.headers['x-api-key'] as string) ||
(req.headers['cf-connecting-ip'] as string) ||
req.ip;
const key = `ratelimit:client:${clientId}`;
const capacity = 100; // Maksimum burst 100 request
const refillRate = 10; // Isi ulang 10 token per detik
const requested = 1;
const now = Math.floor(Date.now() / 1000);
try {
const result = await redis.evalsha(
scriptSha,
1,
key,
capacity,
refillRate,
requested,
now
) as [number, number];
const isAllowed = result[0] === 1;
const remainingTokens = result[1];
// 2. Sematkan Header Standar IETF RFC 6585
res.setHeader('X-RateLimit-Limit', capacity);
res.setHeader('X-RateLimit-Remaining', remainingTokens);
if (!isAllowed) {
const retryAfterSeconds = Math.ceil((1 - remainingTokens) / refillRate);
res.setHeader('Retry-After', Math.max(1, retryAfterSeconds));
return res.status(429).json({
error: 'Too Many Requests',
message: 'Batas kuota akses API terlampaui. Silakan perlambat frekuensi permintaan Anda.',
retry_after_seconds: Math.max(1, retryAfterSeconds),
});
}
next();
} catch (error) {
// Fail-open strategy: Jika Redis down, jangan sampai sistem menolak pengguna sah
console.error('[RateLimiter Error]', error);
next();
}
}
Menentukan Identitas dan Tingkatan Kuota (Tiered Limits) #
Penerapan rate limiting yang efektif harus membedakan batas limit berdasarkan segmen pengguna:
- Endpoint Autentikasi Publik (
/api/auth/login): Terapkan limit ketat berbasis IP (misalnya maksimal 5 request per menit) untuk melumpuhkan serangan credential stuffing. - Klien Publik Tanpa Autentikasi: Batasi berdasarkan IP geografis dengan kuota 60 request per menit.
- Klien Enterprise B2B: Batasi berdasarkan API Key unik dengan kuota hingga 5.000 request per menit, dilengkapi dashboard analitik penggunaan kuota secara transparan.
Dengan menerapkan algoritma Token Bucket yang dieksekusi secara atomik menggunakan Redis Lua, infrastruktur API perusahaan memiliki perlindungan tangguh dari lonjakan anomali trafik tanpa mengorbankan kecepatan latensi respons backend.
