L
MySQL • Literally Web ID

Advanced Rate Limiting with Redis and Lua Scripts

⏱️ 18 menit baca • 📅 12 Oct 2026 • 👁️ 1 views
Advanced Rate Limiting with Redis and Lua Scripts

1. Konteks Industri & Analisis Akar Masalah

Dalam lanskap arsitektur web modern, ketersediaan layanan (high availability) dan ketahanan terhadap lonjakan beban atau serangan siber adalah metrik vital yang menentukan kelangsungan bisnis. Arsitektur web yang mendistribusikan beban melalui CDN (seperti Cloudflare), Web Server (Nginx/Apache), Application Gateway, hingga Application Server (Node.js, Go, PHP-FPM) sering kali menjadi target empuk serangan lapisan aplikasi, terutama serangan Denial of Service (DoS), Distributed Denial of Service (DDoS), serta serangan brute-force pada endpoint sensitif seperti /api/v1/auth/login atau /api/v1/payment.

Bottleneck Performa dan Keamanan Konvensional

Secara historis, mekanisma rate limiting sering kali diimplementasikan di tingkat aplikasi web menggunakan middleware berbasis bahasa pemrograman (seperti Express rate-limiter di Node.js, Laravel Throttle di PHP, atau Gorillla Mux middleware di Go). Pendekatan ini memiliki cacat arsitektural yang fatal pada skala enterprise:

  1. High Memory Overhead: Menyimpan status request count per IP atau token pengguna langsung di memori lokal proses aplikasi menyebabkan konsumsi RAM meledak saat terjadi jutaan concurrent requests.
  2. State Inconsistency dalam Cluster: Ketika aplikasi diskalakan secara horizontal (misalnya menjalankan 10 pod Kubernetes atau 5 instance EC2 di belakang Application Load Balancer), penyimpanan status di memori lokal membuat pembatasan tingkat akses menjadi tidak akurat karena setiap instance memiliki penghitungnya sendiri.
  3. Race Condition (Kondisi Balapan): Operasi baca-modifikasi-tulis (read-modify-write) yang tidak atomik pada penyimpanan data eksternal (seperti database relasional atau operasi Redis standar GET diikuti INCR) rentan terhadap kondisi balapan di bawah konkurensi tinggi, yang mengakibatkan bobolnya pembatasan kuota (rate limit bypass).

Kondisi Sebelum vs Sesudah Penerapan

  • Sebelum: Aplikasi rentan terhadap credential stuffing, kehabisan koneksi database akibat lonjakan request bot, dan lonjakan penggunaan CPU/RAM pada web server karena request jahat diproses hingga lapisan aplikasi sebelum akhirnya ditolak.
  • Sesudah: Menggunakan Redis dengan skrip Lua yang dieksekusi secara atomik di tingkat in-memory memastikan bahwa setiap request divalidasi dan dicatat dalam hitungan mikrodetik sebelum mencapai logika bisnis inti. Serangan diblokir di pintu gerbang, melindungi infrastruktur backend, database, dan sistem pihak ketiga.

2. Konsep Inti & Deep Dive Arsitektur

Untuk merancang sistem rate limiting yang tangguh, kita perlu memahami dua algoritma utama yang menjadi standar industri: Token Bucket dan Leaky Bucket.

Token Bucket vs Leaky Bucket

  • Token Bucket: Mengizinkan burst traffic (lonjakan mendadak). Bucket terisi dengan token pada tingkat konstan tertentu hingga kapasitas maksimum. Setiap request mengonsumsi satu token. Jika bucket kosong, request ditolak. Sangat ideal untuk API publik di mana pengguna sewaktu-waktu mengirimkan beberapa request secara bersamaan.
  • Leaky Bucket: Mengatur aliran output agar konstan dan halus. Request masuk ditampung dalam antrean (queue) dan diproses pada kecepatan tetap. Jika antrean penuh, request dibuang. Cocok untuk sistem pengiriman traffic jaringan di mana lonjakan tiba-tiba harus diratakan.

Alur Eksekusi Arsitektural

Diagram berikut mengilustrasikan alur interaksi dari Client hingga Redis Engine menggunakan Nginx dan Lua Script:

+--------+       +-------------------+       +-----------------------+
| Client | ----> | Nginx + Lua Module | ----> | Redis Engine (Lua)    |
+--------+       +-------------------+       +-----------------------+
                         |                               |
                         | (Reject jika limit terlampaui)| (Eksekusi Atomik)
                         v                               v
                 +---------------+               +-----------------------+
                 | HTTP 429 Too  |               | Update Counter & TTL  |
                 | Many Requests |               | Return Status (0 / 1) |
                 +---------------+               +-----------------------+

3. Perbandingan Komparatif / Analisis Pendekatan

Tabel berikut membandingkan tiga metode rate limiting utama yang sering dipertimbangkan oleh arsitek infrastruktur:

Parameter In-Memory Aplikasi (Lokal) Redis Standar (INCR + EXPIRE) Redis + Skrip Lua Atomik Nginx limit_req Module
Skalabilitas Cluster Buruk (Isolated) Baik Sangat Baik Baik (Standalone/Zone)
Atomic Operation Ya (Single Process) Tidak (Race Condition) Ya (Single-Threaded Lua) Ya
Dukungan Burst Traffic Tergantung Implementasi Sulit Sangat Fleksibel (Token Bucket) Ya (konfigurasi burst)
Latensi Tambahan Sangat Rendah (~0ms) Rendah (~1-2ms) Rendah (~1-3ms) Sangat Rendah (~0ms)
Kompleksitas Setup Rendah Sedang Menengah-Tinggi Sedang

[!WARNING] Jangan pernah menggunakan perintah Redis terpisah seperti GET key dilanjutkan dengan INCR key dan EXPIRE key dalam aplikasi untuk pembatasan tingkat akses. Operasi ini tidak atomik dan pasti akan mengalami kegagalan akurasi (race condition) di bawah beban konkuren tinggi.


4. Prasyarat Sistem & Persiapan Lingkungan

Sebelum mengimplementasikan solusi ini, pastikan infrastruktur Anda memenuhi spesifikasi berikut:

  • Sistem Operasi: Ubuntu 22.04 LTS / Debian 12 / AlmaLinux 9
  • Web Server / Gateway: Nginx Plus atau OpenResty (Nginx yang sudah dibundel dengan LuaJIT)
  • Database Caching: Redis Server versi 6.x atau 7.x
  • Dependensi Tambahan: lua-resty-redis (library koneksi Redis untuk OpenResty/Nginx Lua)

Verifikasi Instalasi OpenResty & Redis

Jalankan perintah berikut di terminal untuk memastikan paket terinstal dengan benar:

# Verifikasi OpenResty
openresty -v

# Verifikasi Redis Server
redis-cli --version

# Uji koneksi Redis
redis-cli ping
# Output yang diharapkan: PONG

5. Panduan Implementasi Langkah demi Langkah

Langkah 1: Menulis Skrip Lua untuk Algoritma Token Bucket

Buat file skrip Lua di direktori konfigurasi Nginx, misalnya /etc/nginx/lua/rate_limit.lua. Skrip ini menangani logika pengambilan token secara atomik.

-- /etc/nginx/lua/rate_limit.lua
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local interval = tonumber(ARGV[2])
local now = tonumber(ARGV[3])

-- Ambil data bucket saat ini dari Redis
local bucket = redis.call('HMGET', key, 'tokens', 'last_updated')
local tokens
local last_updated

if not bucket[1] then
    -- Jika bucket belum ada, inisialisasi penuh
    tokens = limit
    last_updated = now
else
    tokens = tonumber(bucket[1])
    last_updated = tonumber(bucket[2])
end

-- Hitung regenerasi token berdasarkan waktu yang berlalu
local delta = math.max(0, now - last_updated)
local tokens_to_add = delta * (limit / interval)
tokens = math.min(limit, tokens + tokens_to_add)
last_updated = now

local allowed = 0
if tokens >= 1 then
    tokens = tokens - 1
    allowed = 1
end

-- Simpan kembali ke Redis dengan TTL otomatis
redis.call('HMSET', key, 'tokens', tokens, 'last_updated', last_updated)
redis.call('EXPIRE', key, math.ceil(interval))

return { allowed, tokens }

Langkah 2: Konfigurasi Nginx / OpenResty Gateway

Konfigurasikan Nginx untuk memuat skrip Lua ini pada setiap request API yang masuk. Buka file konfigurasi server block Nginx Anda (misalnya /etc/nginx/conf.d/api_gateway.conf).

http {
    lua_shared_dict rate_limit_cache 10m;
    lua_package_path "/etc/nginx/lua/?.lua;;";

    upstream backend_api {
        server 127.0.0.1:8080;
        keepalive 32;
    }

    server {
        listen 80;
        server_name api.enterprise.internal;

        # Tentukan direktori log
        access_log /var/log/nginx/api_access.log;
        error_log /var/log/nginx/api_error.log;

        location /api/v1/ {
            # Akses Redis melalui access_by_lua_block
            access_by_lua_block {
                local redis = require "resty.redis"
                local red = redis:new()

                red:set_timeout(1000) -- 1 detik timeout

                local ok, err = red:connect("127.0.0.1", 6379)
                if not ok then
                    ngx.log(ngx.ERR, "Gagal terhubung ke Redis: ", err)
                    return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
                end

                -- Batasi berdasarkan IP Klien
                local client_ip = ngx.var.remote_addr
                local rate_key = "rate_limit:" .. client_ip

                -- Parameter: Limit 10 request per 60 detik
                local limit = 10
                local interval = 60
                local now = ngx.time()

                -- Baca file skrip Lua atau load langsung
                local script = [[ 
                    local key = KEYS[1]
                    local limit = tonumber(ARGV[1])
                    local interval = tonumber(ARGV[2])
                    local now = tonumber(ARGV[3])
                    local bucket = redis.call('HMGET', key, 'tokens', 'last_updated')
                    local tokens
                    local last_updated
                    if not bucket[1] then
                        tokens = limit
                        last_updated = now
                    else
                        tokens = tonumber(bucket[1])
                        last_updated = tonumber(bucket[2])
                    end
                    local delta = math.max(0, now - last_updated)
                    local tokens_to_add = delta * (limit / interval)
                    tokens = math.min(limit, tokens + tokens_to_add)
                    last_updated = now
                    local allowed = 0
                    if tokens >= 1 then
                        tokens = tokens - 1
                        allowed = 1
                    end
                    redis.call('HMSET', key, 'tokens', tokens, 'last_updated', last_updated)
                    redis.call('EXPIRE', key, math.ceil(interval))
                    return { allowed, tokens }
                ]]

                local res, err = red:eval(script, 1, rate_key, limit, interval, now)
                if not res then
                    ngx.log(ngx.ERR, "Gagal eksekusi skrip Lua Redis: ", err)
                    return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
                end

                -- Kembalikan koneksi ke connection pool Redis (maks 100 koneksi, 10 detik idle)
                red:set_keepalive(10000, 100)

                local allowed = res[1]
                local remaining = res[2]

                -- Set header respons standar rate limiting
                ngx.header["X-RateLimit-Limit"] = limit
                ngx.header["X-RateLimit-Remaining"] = math.floor(remaining)

                if allowed == 0 then
                    ngx.header["Retry-After"] = interval
                    ngx.status = ngx.HTTP_TOO_MANY_REQUESTS
                    ngx.say(Cjson.encode({ error = "Terlalu banyak permintaan. Silakan coba lagi nanti." }))
                    return ngx.exit(ngx.HTTP_TOO_MANY_REQUESTS)
                end
            }

            proxy_pass http://backend_api;
            proxy_http_version 1.1;
            proxy_set_header Connection "";
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }
    }
}

6. Bedah Kode & Penjelasan Detail Baris Kunci

Mari kita bedah beberapa komponen penting dari kode skrip Lua dan konfigurasi Nginx di atas:

  1. Atomisitas Skrip Lua (red:eval): Redis menjalankan seluruh blok skrip Lua sebagai satu instruksi tunggal yang tidak dapat disela (single-threaded execution). Ini menjamin bahwa tidak ada operasi konkuren lain yang dapat menyisipkan perubahan di antara perintah HMGET dan HMSET, sehingga akurasi perhitungan token terjamin 100%.
  2. Regenerasi Token Dinamis (delta * (limit / interval)): Alih-alih menggunakan cron job untuk mereset counter setiap menit (yang membebani CPU), token dihitung secara matematis berdasarkan selisih waktu (now - last_updated) saat request baru tiba. Ini adalah implementasi lazy evaluation yang sangat efisien.
  3. Connection Pooling Redis (red:set_keepalive): Membuka koneksi TCP baru ke Redis pada setiap HTTP request akan memicu port exhaustion dan latensi tinggi. Penggunaan set_keepalive memastikan koneksi Redis dijaga tetap terbuka dan digunakan kembali oleh worker process Nginx.

7. Skenario Produksi & Studi Kasus Dunia Nyata

Studi Kasus: Peluncuran Tiket Konser / Flash Sale E-Commerce

Sebuah platform e-commerce menghadapi lonjakan traffic hingga 50.000 request per detik pada endpoint /api/v1/checkout. Tanpa sistem pembatasan yang tepat, database backend akan mengalami deadlock dan tumbang.

Solusi Penerapan di Lingkungan Produksi:

  • Menggunakan Redis Cluster dengan arsitektur Master-Replica untuk mendistribusikan beban kunci rate limit.
  • Mengelompokkan kunci rate limit tidak hanya berdasarkan IP, melainkan gabungan IP dan ID Pengguna (rate_limit:user:108293:checkout).
  • Menerapkan circuit breaker di Nginx: Jika Redis mengalami kegagalan total (misalnya latensi melebihi ambang batas 50ms), Nginx dikonfigurasi untuk fail-open (mengizinkan request dengan peringatan log) atau fail-closed (menolak seluruh request kritikal demi keamanan server pusat) sesuai kebijakan operasional.

8. Common Pitfalls, Anti-Patterns & Solusi

  • Anti-Pattern 1: Menggunakan Perintah KEYS untuk Pembersihan Data
    • Masalah: Menjalankan perintah KEYS rate_limit:* di Redis production untuk memantau atau membersihkan data akan memblokir (blocking) seluruh event loop Redis karena kompleksitas waktunya $O(N)$.
    • Solusi: Selalu gunakan perintah SCAN atau manfaatkan fitur otomatisasi TTL (EXPIRE) bawaan Redis seperti yang diterapkan pada skrip Lua di atas.
  • Anti-Pattern 2: Mengabaikan Header Klien di Balik Cloudflare / Load Balancer
    • Masalah: Mengambil alamat IP menggunakan ngx.var.remote_addr ketika Nginx berada di belakang Cloudflare atau AWS ALB akan menghasilkan IP lokal load balancer, bukan IP asli klien.
    • Solusi: Gunakan header X-Forwarded-For atau CF-Connecting-IP dengan validasi yang ketat terhadap rentang IP proxy tepercaya.

9. Benchmark, Metrik Kinerja & Tuning Performa

Pengujian performa dilakukan menggunakan tool wrk untuk mensimulasikan beban tinggi pada API Gateway.

# Simulasi 200 koneksi konkuren selama 30 detik
wrk -t12 -c200 -d30s http://api.enterprise.internal/api/v1/health

Hasil Benchmark Kinerja (Target Rata-rata)

  • Latensi Rata-rata: 1.82 ms
  • Throughput: 18,450 Requests / Second
  • Error Rate: 0.00% (kecuali respons HTTP 429 yang memang disengaja saat batas terlampaui)

Tuning Kernel Linux untuk High Concurrency (/etc/sysctl.conf)

Untuk memastikan sistem operasi mampu menangani puluhan ribu koneksi jaringan secara bersamaan, terapkan parameter berikut:

fs.file-max = 2097152
net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 65535
net.ipv4.ip_local_port_range = 1024 65535

Terapkan perubahan dengan perintah:

sysctl -p

10. Checklist Penerapan Produksi & Kesimpulan

Gunakan checklist berikut sebelum melakukan deployment sistem rate limiting ke lingkungan produksi:

  • Skrip Lua telah diuji secara lokal terhadap kondisi race condition dan konkurensi tinggi.
  • Koneksi Redis menggunakan password autentikasi yang kuat dan dibatasi melalui jaringan internal privat (VPC).
  • Timeout koneksi Redis di Nginx dikonfigurasi dengan agresif (maksimal 1-2 detik) untuk mencegah proses Nginx menggantung (blocking).
  • Header respons standar (X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After) telah diimplementasikan dengan benar.
  • Pemantauan metrik (monitoring) Redis (seperti used_memory, instantaneous_ops_sec, dan rejected_connections) aktif di Prometheus/Grafana.

Dengan mengombinasikan efisiensi Nginx/OpenResty, ketepatan algoritma Token Bucket, dan atomisitas eksekusi skrip Lua di Redis, infrastruktur web enterprise Anda kini memiliki pertahanan berlapis yang tidak hanya cepat dan skalabel, tetapi juga kebal terhadap berbagai bentuk eksploitasi beban berlebih.

🏷️ Tags: #Nginx #Redis #Security #Lua #Api Gateway