L
JavaScript • Literally Web ID

Advanced Rate Limiting dengan Redis & Lua Scripts di Skala Enterprise

⏱️ 20 menit baca • 📅 12 Oct 2026 • 👁️ 2 views
Advanced Rate Limiting dengan Redis & Lua Scripts di Skala Enterprise

1. Konteks Industri & Analisis Akar Masalah

Dalam lanskap arsitektur web modern, ketersediaan tinggi (high availability) dan ketahanan sistem (resilience) terhadap lonjakan lalu lintas yang tidak terduga atau serangan malicious merupakan prioritas mutlak bagi Principal Engineer. Backend web (Golang, Node.js, PHP-FPM), server web (Nginx, Caddy), sistem database (MySQL, MariaDB), hingga lapisan caching (Redis) sering kali menjadi target eksploitasi melalui serangan brute-force, credential stuffing, Denial of Service (DoS), atau penyalahgunaan API komersial (API abuse).

Secara historis, penerapan rate limiting di tingkat aplikasi menggunakan penyimpanan lokal (in-memory) pada instance server tunggal terbukti gagal total di era infrastruktur terdistribusi. Ketika sebuah aplikasi diskalakan secara horizontal di balik load balancer (seperti Nginx atau AWS ALB) yang mengarahkan trafik ke puluhan container Docker atau Kubernetes pods, mekanisme pembatasan rate limit berbasis memori lokal akan kehilangan sinkronisasi. Skenario ini menghasilkan bypass vulnerability, di mana penyerang dapat mendistribusikan request ke berbagai node server yang berbeda untuk menghindari batas kuota.

Kondisi Sebelum vs Sesudah Penerapan

Aspek Sebelum (Tanpa Distributed Rate Limiting) Sesudah (Dengan Redis & Lua Scripting)
Akurasi Pembatasan Parsial/Lemah (gagal di lingkungan multi-server/horizontal scaling) Sangat Akurat (terpusat, konsisten, dan terdistribusi)
Ketahanan terhadap Race Condition Rentan terhadap Race Condition akibat pembacaan dan penulisan terpisah 100% Aman karena eksekusi atomik menggunakan skrip Lua di Redis
Beban Jaringan (Network Overhead) Tinggi (memerlukan banyak round-trip TCP antara aplikasi dan database/cache) Sangat Rendah (1 kali round-trip untuk mengeksekusi seluruh logika kompleks)
Mitigasi Serangan Mudah lumpuh saat terjadi lonjakan trafik brute-force atau DDoS layer 7 Efektif menolak trafik berlebih secara instan sebelum membebani database utama

Dengan mengintegrasikan Redis sebagai pusat penyimpanan data terdistribusi dan Lua Scripting untuk menjamin eksekusi atomik, kita dapat membangun sistem rate limiter enterprise yang mampu menangani jutaan request per detik dengan latensi sub-milidetik.


2. Konsep Inti & Deep Dive Arsitektur

Untuk merancang sistem rate limiting yang handal, kita harus memahami dua algoritma utama yang menjadi industri standar: Token Bucket dan Leaky Bucket, serta bagaimana Redis mengeksekusi logika tersebut melalui Lua Virtual Machine internal.

Token Bucket vs Leaky Bucket Algorithm

  1. Token Bucket Algorithm:

    • Menggunakan sebuah bucket dengan kapasitas maksimum $B$ yang secara konstan diisi ulang dengan token pada tingkat kecepatan $R$ per detik.
    • Setiap request yang masuk mengkonsumsi satu atau lebih token dari bucket.
    • Jika bucket kosong, request ditolak (HTTP 429 Too Many Requests).
    • Keunggulan: Mengizinkan burst traffic (lonjakan trafik sesaat) selama token masih tersedia di dalam bucket.
  2. Leaky Bucket Algorithm:

    • Berfungsi sebagai antrean penampung request yang keluar dengan kecepatan yang konstan dan rata (smooth).
    • Jika antrean penuh, request yang baru datang akan meluber (overflow) dan dibuang.
    • Keunggulan: Menghasilkan keluaran trafik yang sangat stabil tanpa lonjakan mendadak, sangat ideal untuk pembatasan bandwidth jaringan.

Alur Eksekusi di Balik Layar (Under the Hood)

[!IMPORTANT] Tantangan terbesar dalam implementasi rate limiter terdistribusi adalah Race Condition. Jika dua request dari klien yang sama tiba secara bersamaan pada milidetik yang sama, pembacaan nilai counter (GET) diikuti oleh penambahan nilai (INCR/SET) tanpa penguncian (locking) akan mengakibatkan hilangnya hitungan request (lost updates).

Redis menyelesaikan masalah ini dengan menjalankan skrip Lua secara atomik. Ketika sebuah skrip Lua dikirim ke Redis melalui perintah EVAL atau EVALSHA, Redis mengunci seluruh server Redis dari eksekusi perintah lain hingga skrip Lua tersebut selesai sepenuhnya. Tidak ada perintah lain yang dapat diselipkan di tengah-tengah eksekusi skrip tersebut.

+-------------------------------------------------------------+
|                      Client / API Gateway                   | 
|  (Nginx / Go Backend / Node.js Service with HTTP Requests)  | 
+------------------------------+------------------------------+
                               |
              1. Kirim Key, Limit, dan Window (EVALSHA)
                               v
+------------------------------+------------------------------+
|                        Redis Server                         | 
|  +-------------------------------------------------------+  |
|  |                   Lua Virtual Machine                 |  |
|  |  - Cek eksistensi Key                                 |  |
|  |  - Hitung waktu kadaluarsa (TTL)                      |  |
|  |  - Increment & Evaluasi Batas Kuota (Atomik)          |  |
|  +---------------------------+---------------------------+  |
|                              v                              |
|             In-Memory Hash / String Data Store              |
+------------------------------+------------------------------+
                               |
     2. Return Hasil: [Allowed (1/0), Remaining, Reset Time]
                               v
+------------------------------+------------------------------+
|                      Client / API Gateway                   | 
|      (Teruskan Request ke Backend atau Blokir dengan 429)   |
+-------------------------------------------------------------+

3. Perbandingan Komparatif / Analisis Pendekatan

Berikut adalah analisis mendalam mengenai berbagai pendekatan arsitektur rate limiting yang umum diterapkan di lingkungan enterprise:

Pendekatan / Teknologi Latency Overhead Konsumsi Memori RAM Skalabilitas Horizontal Kompleksitas Implementasi
In-Memory Lokal (Backend App) Sangat Rendah (< 0.1ms) Rendah per Node Buruk (Tidak Sinkron antar Node) Sangat Rendah
Relational DB (MySQL/MariaDB) Tinggi (5ms - 50ms) Tinggi (Disk & Buffer Pool) Baik (Asalkan ada Replication) Sedang
Redis Standard (Multi-Command) Sedang (1ms - 3ms) Sangat Efisien Sangat Baik Sedang (Rentan Race Condition)
Redis + Lua Scripting (Atomik) Rendah (0.5ms - 1.5ms) Sangat Efisien Sangat Baik (Enterprise-Grade) Tinggi
Cloudflare Edge Rate Limiting Ekstrem Rendah (Edge) Dikelola Pihak Ketiga Global / Anycast Sangat Rendah (Konfigurasi UI/API)

Trade-off Analisis: Pendekatan Redis + Lua Scripting memerlukan pemahaman mendalam mengenai bahasa pemrograman Lua dan manajemen skrip di Redis (SCRIPT LOAD / EVALSHA), namun memberikan jaminan konsistensi data yang mutlak tanpa membebani database transaksional utama seperti MySQL.


4. Prasyarat Sistem & Persiapan Lingkungan

Sebelum masuk ke tahap implementasi, pastikan infrastruktur Anda memenuhi spesifikasi minimum berikut:

  • OS: Ubuntu Server 22.04 LTS / Debian 12 / AlmaLinux 9
  • Redis Server: Versi 6.0 atau lebih tinggi (Direkomendasikan Redis 7.x dengan fungsi Redis Functions dan dukungan Lua 5.1)
  • Web Server / Reverse Proxy: Nginx 1.24+ dengan modul ngx_http_lua_module atau integrasi Lua/OpenResty, atau API Gateway seperti Kong / APISIX.
  • Bahasa Pemrograman Backend: Go 1.21+ atau Node.js 20+.

Instalasi Dependensi Dasar di Ubuntu/Debian

# Update sistem dan instal utilitas dasar
sudo apt update && sudo apt upgrade -y
sudo apt install -y curl wget gnupg lsb-release build-essential pkg-config

# Instal Redis Server resmi
curl -fsSL https://packages.redis.io/gpg | sudo gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/ $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/redis.list
sudo apt update
sudo apt install -y redis-server

# Konfigurasi keamanan Redis dasar (/etc/redis/redis.conf)
# Pastikan binding ke IP lokal atau private network, dan aktifkan requirepass
sudo sed -i 's/^bind 127.0.0.1 ::1/bind 0.0.0.0/' /etc/redis/redis.conf
sudo systemctl restart redis-server
sudo systemctl enable redis-server

5. Panduan Implementasi Langkah demi Langkah

Langkah 1: Menulis Skrip Lua untuk Sliding Window Rate Limiter

Simpan file skrip Lua berikut di direktori /etc/redis/scripts/rate_limit.lua. Algoritma Sliding Window Counter menggunakan kombinasi kunci Redis dan struktur data Sorted Set (ZSET) atau Fixed Window dengan presisi tinggi untuk memastikan akurasi limit.

-- /etc/redis/scripts/rate_limit.lua
-- Parameter input dari pemanggil (EVAL/EVALSHA):
-- KEYS[1]: Redis key untuk rate limit (contoh: 'rate:ip:192.168.1.50')
-- ARGV[1]: Current timestamp (epoch milliseconds)
-- ARGV[2]: Window size dalam milidetik (contoh: 60000 untuk 1 menit)
-- ARGV[3]: Batas maksimum request (limit)
-- ARGV[4]: TTL key dalam detik

local key = KEYS[1]
local now = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local limit = tonumber(ARGV[3])
local ttl = tonumber(ARGV[4])

-- Hitung batas waktu awal dari window saat ini
local clear_before = now - window

-- Hapus elemen lama di luar rentang waktu window saat ini dari ZSET
redis.call('ZREMRANGEBYSCORE', key, 0, clear_before)

-- Hitung jumlah request yang masih aktif di dalam window
local current_requests = redis.call('ZCARD', key)

if current_requests < limit then
    -- Jika masih di bawah batas, tambahkan request baru dengan score timestamp saat ini
    redis.call('ZADD', key, now, now .. '-' .. math.random(1000, 9999))
    -- Perbarui TTL key untuk pembersihan otomatis
    redis.call('EXPIRE', key, ttl)
    return {1, limit - current_requests - 1, math.ceil(window / 1000)}
else
    -- Jika melebihi batas, kembalikan status 0 (ditolak)
    return {0, 0, math.ceil(window / 1000)}
end

Langkah 2: Load Skrip Lua ke Redis

Untuk performa optimal di produksi, muat skrip Lua ke dalam Redis Script Cache menggunakan perintah SCRIPT LOAD. Redis akan mengembalikan SHA1 digest dari skrip tersebut, yang kemudian digunakan oleh aplikasi untuk eksekusi cepat.

redis-cli SCRIPT LOAD "$(cat /etc/redis/scripts/rate_limit.lua)"
# Output contoh: 900150983cd24fb0dcf663fa81b8b21cba560383

Langkah 3: Implementasi Klien Backend menggunakan Golang

Berikut adalah implementasi nyata pada backend Go menggunakan redis client resmi (github.com/redis/go-redis/v9):

package main

import (
	"context"
	"crypto/sha1"
	"encoding/hex"
	"fmt"
	"log"
	"net/http"
	"time"

	"github.com/redis/go-redis/v9"
)

const rateLimitLuaScript = `
local key = KEYS[1]
local now = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local limit = tonumber(ARGV[3])
local ttl = tonumber(ARGV[4])
local clear_before = now - window
redis.call('ZREMRANGEBYSCORE', key, 0, clear_before)
local current_requests = redis.call('ZCARD', key)
if current_requests < limit then
    redis.call('ZADD', key, now, now .. '-' .. math.random(1000, 9999))
    redis.call('EXPIRE', key, ttl)
    return {1, limit - current_requests - 1, math.ceil(window / 1000)}
else
    return {0, 0, math.ceil(window / 1000)}
end
`

type RateLimiter struct {
	client *redis.Client
	sha    string
}

func NewRateLimiter(client *redis.Client) *RateLimiter {
	hash := sha1.Sum([]byte(rateLimitLuaScript))
	shaStr := hex.EncodeToString(hash[:])
	
	ctx := context.Background()
	// Pastikan skrip sudah dimuat ke Redis cache
	_, err := client.ScriptLoad(ctx, rateLimitLuaScript).Result()
	if err != nil {
		log.Fatalf("Gagal memuat skrip Lua ke Redis: %v", err)
	}

	return &RateLimiter{
		client: client,
		sha:    shaStr,
	}
}

func (rl *RateLimiter) Allow(ctx context.Context, identifier string, limit int, windowMs int) (bool, int, error) {
	key := fmt.Sprintf("rate:limiter:%s", identifier)
	now := time.Now().UnixMilli()
	ttl := (windowMs / 1000) * 2 // TTL dua kali lipat dari window untuk keamanan pembersihan

	// Jalankan evaluasi EVALSHA
	res, err := rl.client.EvalSha(ctx, rl.sha, []string{key}, now, windowMs, limit, ttl).Result()
	if err != nil {
		// Jika SHA tidak ditemukan di server Redis (misal Redis restart), muat ulang skrip
		if err.Error() == "NOSCRIPT No matching script. Please use EVAL." {
			_, loadErr := rl.client.ScriptLoad(ctx, rateLimitLuaScript).Result()
			ifi loadErr != nil {
				return false, 0, loadErr
			}
			res, err = rl.client.EvalSha(ctx, rl.sha, []string{key}, now, windowMs, limit, ttl).Result()
			if err != nil {
				return false, 0, err
			}
		} else {
			return false, 0, err
		}
	}

	results, ok := res.([]interface{})
	if !ok || len(results) < 2 {
		return false, 0, fmt.Errorf("format hasil Lua tidak valid")
	}

	allowed := results[0].(int64) == 1
	remaining := int(results[1].(int64))

	return allowed, remaining, nil
}

func main() {
	rdb := redis.NewClient(&redis.Options{
		Addr:     "localhost:6379",
		Password: "", 
		DB:       0,
	})

	limiter := NewRateLimiter(rdb)

	http.HandleFunc("/api/v1/resource", func(w http.ResponseWriter, r *http.Request) {
		clientIP := r.RemoteAddr
		
		// Batasan: 5 request per 60 detik (60000 ms)
		allowed, remaining, err := limiter.Allow(r.Context(), clientIP, 5, 60000)
		if err != nil {
			http.Error(w, "Internal Server Error", http.StatusInternalServerError)
			return
		}

		w.Header().Set("X-RateLimit-Limit", "5")
		w.Header().Set("X-RateLimit-Remaining", fmt.Sprintf("%d", remaining))

		if !allowed {
			http.Error(w, "429 Too Many Requests: Rate limit terlampaui", http.StatusTooManyRequests)
			return
		}

		w.Write([]byte("Akses diberikan ke sumber daya enterprise."))
	})

	log.Println("Server berjalan di port :8080...")
	http.ListenAndServe(":8080", nil)
}

6. Bedah Kode & Penjelasan Detail Baris Kunci

Mari kita bedah bagian paling esensial dari skrip Lua yang kita gunakan:

  1. local clear_before = now - window

    • Baris ini menghitung batas waktu kadaluarsa mutlak dalam milidetik. Setiap timestamp yang lebih kecil dari clear_before dianggap sudah berada di luar jendela waktu pemantauan saat ini.
  2. redis.call('ZREMRANGEBYSCORE', key, 0, clear_before)

    • Perintah Redis Sorted Set ini secara efisien menghapus seluruh elemen (request lama) yang nilai skornya berada di antara 0 dan clear_before. Ini memastikan memori Redis tidak membengkak (memory leak) akibat akumulasi data histori yang tidak lagi relevan.
  3. redis.call('ZADD', key, now, now .. '-' .. math.random(1000, 9999))

    • Menambahkan anggota baru ke dalam Sorted Set dengan score sebesar timestamp saat ini (now). Penggunaan string acak (math.random) sebagai bagian dari member value mencegah benturan (collision) apabila dua request dari klien yang sama terjadi pada milidetik yang persis sama.

[!WARNING] Jangan pernah menggunakan perintah yang bersifat non-deterministic (seperti TIME, NOW, atau RANDOM langsung di dalam skrip Lua tanpa penanganan khusus) dalam mode Redis Cluster jika tidak dikelola dengan benar, karena dapat menyebabkan ketidakkonsistenan replikasi data di antara master dan replica nodes. Dalam skrip kita, argumen waktu now dikirimkan dari aplikasi pemanggil (ARGV[1]), menjamin skrip bersifat pure deterministic.


7. Skenario Produksi & Studi Kasus Dunia Nyata

Studi Kasus: Proteksi API Login dari Serangan Brute-Force Berskala Besar

Sebuah platform e-commerce enterprise dengan jutaan pengguna harian menghadapi ancaman serangan credential stuffing pada endpoint /api/v1/auth/login. Penyerang menggunakan jaringan botnet yang tersebar di ribuan alamat IP untuk mencoba kombinasi kata sandi.

Solusi Arsitektur yang Diterapkan:

  1. Kombinasi Key Dinamis: Rate limiter tidak hanya membatasi berdasarkan IP address semata, tetapi menggunakan gabungan antara IP dan Username yang dicoba (rate:login:ip:192.168.1.10:user:admin@company.com).
  2. Peningkatan Eksponensial (Tiered Limits):
    • Percobaan 1 - 5: Batas normal (5 request per menit).
    • Jika terlampaui, sistem memperketat aturan menjadi 1 request per 10 menit untuk kombinasi IP tersebut dan mencatat insiden ke sistem SIEM melalui webhook Postfix/Syslog.
  3. Integrasi Nginx Reverse Proxy & Lua: Pengecekan rate limit ditarik langsung ke lapisan Nginx menggunakan modul OpenResty sebelum request menyentuh server aplikasi backend PHP/Go, menghemat sumber daya CPU secara signifikan.

8. Common Pitfalls, Anti-Patterns & Solusi

Berikut adalah kesalahan fatal yang sering dilakukan oleh pengembang pemula hingga menengah saat mengimplementasikan rate limiter:

  • Anti-Pattern 1: Menggunakan Perintah KEYS untuk Pembersihan Data

    • Masalah: Menjalankan perintah KEYS rate:* di dalam kode aplikasi atau skrip untuk menghitung jumlah key aktif akan memblokir (blocking) single-threaded event loop Redis selama beberapa detik jika jumlah key mencapai jutaan.
    • Solusi: Selalu gunakan struktur data terpusat seperti Hashes atau Sorted Sets per identifier, atau manfaatkan perintah non-blocking SCAN jika iterasi global mutlak diperlukan.
  • Anti-Pattern 2: Mengabaikan Kegagalan Koneksi Redis (Fail-Open vs Fail-Close)

    • Masalah: Jika server Redis mengalami crash atau jaringan terputus, aplikasi backend ikut mengalami downing karena exception koneksi Redis.
    • Solusi: Terapkan pola Fail-Open pada blok penanganan error aplikasi. Jika Redis gagal diakses, log error tersebut ke sistem monitoring (Prometheus/Grafana) dan izinkan trafik lewat untuk sementara waktu guna menjaga ketersediaan layanan bisnis utama.
  • Anti-Pattern 3: Memory Bloat pada Redis

    • Masalah: Tidak menyetel batasan Maxmemory dan Eviction Policy pada konfigurasi Redis (redis.conf), yang mengakibatkan server mengalami Out-Of-Memory (OOM) crash.
    • Solusi: Selalu atur kebijakan memori Redis:
      maxmemory 2gb
      maxmemory-policy volatile-lru
      

9. Benchmark, Metrik Kinerja & Tuning Performa

Untuk memastikan sistem rate limiter mampu menahan beban produksi, pengujian performa wajib dilakukan menggunakan tool load testing modern seperti k6 atau wrk.

Skrip Pengujian Load Testing menggunakan k6 (load_test.js)

import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
  stages: [
    { duration: '30s', target: 500 },  // Ramp-up hingga 500 virtual users
    { duration: '1m', target: 2000 },  // Pertahankan 2000 virtual users (High Concurrency)
    { duration: '30s', target: 0 },    // Ramp-down
  ],
};

export default function () {
  const url = 'http://localhost:8080/api/v1/resource';
  const params = {
    headers: {
      'X-Forwarded-For': '192.168.1.' + Math.floor(Math.random() * 50), // Simulasi berbagai IP
    },
  };

  const res = http.get(url, params);
  
  check(res, {
    'status adalah 200 atau 429': (r) => r.status === 200 || r.status === 429,
    'latensi di bawah 15ms': (r) => r.timings.duration < 15,
  });

  sleep(0.1);
}

Tuning Parameter Linux Kernel untuk High Concurrency

Pastikan sistem operasi Linux tempat Redis dan Web Server berjalan telah di-tuning untuk menangani koneksi jaringan tingkat tinggi (/etc/sysctl.conf):

# Tingkatkan jumlah maksimum file descriptors
fs.file-max = 2097152

# Optimasi TCP backlog queue
net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 65535

# Izinkan port reuse untuk koneksi cepat
net.ipv4.tcp_tw_reuse = 1

Terapkan perubahan dengan menjalankan perintah sudo sysctl -p.


10. Checklist Penerapan Produksi & Kesimpulan

Sebelum merilis sistem rate limiter Redis & Lua ke lingkungan Production, pastikan seluruh poin dalam checklist berikut telah terpenuhi:

  • Skrip Lua telah diuji terhadap kondisi Race Condition dengan pengujian konkuren tinggi.
  • Redis Persistence & Replication dikonfigurasi dengan benar (RDB/AOF diaktifkan atau menggunakan Redis Sentinel / Redis Cluster untuk High Availability).
  • Maxmemory Policy diatur ke volatile-lru atau allkeys-lru untuk mencegah OOM crash.
  • Error Handling (Fail-Open) diterapkan pada kode backend jika koneksi ke Redis terputus.
  • Header HTTP Standar (X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After) disertakan dengan akurat pada setiap respons.
  • Monitoring & Alking (Grafana Dashboard untuk memantau Redis ops/sec, hit/miss ratio, dan memory usage) telah aktif.

Dengan mengimplementasikan arsitektur rate limiting berbasis Redis dan skrip Lua secara tepat, enterprise Anda tidak hanya mengamankan infrastruktur dari ancaman serangan siber destruktif, tetapi juga menjamin stabilitas performa sistem secara keseluruhan bagi seluruh pengguna sah.

🏷️ Tags: #Redis #Golang #System Architecture #Rate Limiting #Lua Scripting