Menskala aplikasi Express secara horizontal: load balancer, stateless session dan session store bersama, Socket.IO Redis adapter, observability dengan metrik, serta load testing dan tuning.

Episode 24 membuat aplikasi berjalan di satu instance. Episode 25 menjawab pertanyaan berikutnya: apa yang terjadi saat satu instance tidak cukup? Scaling horizontal — menambah instance, bukan memperbesar satu mesin — adalah jawaban standar, dan ia datang dengan tantangan unik.
Mengapa scaling sulit untuk aplikasi Express? Karena aplikasi dengan state di memori lokal akan patah saat disebar ke banyak instance: sesi yang dibuat instance A tidak dikenal instance B, dan event Socket.IO instance A tidak sampai ke client instance B. Episode ini menyelesaikan semuanya — membuat aplikasi stateless sehingga banyak instance bekerja sebagai satu kesatuan.
Semua instance berbagi state eksternal (database dan Redis) — tidak ada yang disimpan di memori instance. Inilah definisi aplikasi stateless.
upstream express_api {
least_conn;
server 10.0.0.11:3000;
server 10.0.0.12:3000;
server 10.0.0.13:3000;
}
server {
listen 443 ssl;
server_name api.example.com;
location / {
proxy_pass http://express_api;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Request-Id $request_id;
}
}least_conn mengirim ke instance dengan koneksi paling sedikit.X-Forwarded-* dibutuhkan aplikasi untuk tahu IP asli client — dan wajib mengaktifkan trust proxy:app.set("trust proxy", 1)Tanpa trust proxy, req.ip menunjuk ke IP load balancer dan rate limit (episode 20) menganggap semua user satu IP. Ini alasan mengapa trust proxy disebut berkali-kali di series ini.
Important
trust proxy adalah keputusan keamanan: hanya aktifkan jika kalian yakin permintaan berasal dari proxy tepercaya. Mengaktifkannya tanpa proxy justru membiarkan client mengirim header X-Forwarded-For palsu dan memalsukan IP.
express-session default memakai MemoryStore (episode 12) — data sesi hidup di memori instance. Di instance A user login; di instance B (tempat request berikutnya dikirim) sesinya "tidak ada".
Solusinya: pindahkan session ke Redis yang dibagikan semua instance:
npm install connect-redisimport session from "express-session"
import RedisStore from "connect-redis"
import { Redis } from "ioredis"
import { config } from "../config.js"
const redisClient = new Redis(config.redisUrl)
app.use(
session({
store: new RedisStore({ client: redisClient }),
secret: config.sessionSecret,
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
sameSite: "lax",
secure: config.isProduction,
},
})
)Sekarang sesi "tinggal" di Redis — instance mana pun yang melayani request membaca sesi yang sama. Manfaat tambahan: sesi bertahan saat instance di-restart (dibanding MemoryStore yang hilang).
Jika memakai JWT, kalian sudah stateless — token membawa semuanya. Ini alasan JWT begitu populer untuk API diskala. Konsekuensinya: invalidasi token (logout server-side, pemblokiran akun) harus dikelola eksplisit (misal denylist di Redis) karena server tidak menyimpan state sesi.
| Pendekatan | State | Scaling | Logout/block server-side |
|---|---|---|---|
| Session cookie + Redis store | Redis | Perlu store bersama | Mudah (hapus dari Redis) |
| JWT stateless | Token | Tanpa store | Perlu denylist eksplisit |
Episode 14 berjanji: event Socket.IO lintas instance butuh Redis adapter. Tepatnya sekarang:
npm install @socket.io/redis-adapterimport { Server } from "socket.io"
import { createAdapter } from "@socket.io/redis-adapter"
import { Redis } from "ioredis"
import { config } from "../config.js"
const pubClient = new Redis(config.redisUrl)
const subClient = pubClient.duplicate()
const io = new Server(httpServer, {
cors: { origin: config.corsOrigins },
adapter: createAdapter(pubClient, subClient),
})Adapter memakai dua koneksi Redis (pub/sub): saat instance A melakukan io.emit, event disebarkan lewat Redis sehingga client yang terhubung ke instance B juga menerimanya. Tanpa ini, realtime aplikasi kalian hanya menjangkau sebagian pengguna.
npm install prom-clientimport client from "prom-client"
client.collectDefaultMetrics()
const httpRequests = new client.Counter({
name: "http_requests_total",
help: "Total HTTP request",
labelNames: ["method", "status"],
})
app.use((req, res, next) => {
res.on("finish", () => {
httpRequests.inc({ method: req.method, status: res.statusCode })
})
next()
})
app.get("/metrics", async (req, res) => {
res.set("Content-Type", client.register.contentType)
res.send(await client.register.metrics())
})Endpoint /metrics dibaca Prometheus, divisualisasikan di Grafana — bersama dengan log structured episode 17. Tiga sinyal observability (log + metrik + health) membentuk dasar operasi aplikasi diskala.
Orchestrator memakai health check (episode 24) untuk mengganti instance yang tidak sehat — prasyarat high availability: instance mati tidak berarti aplikasi mati, selama ada instance lain yang melayani.
Uji kapasitas sebelum dan sesudah tuning:
npm install -g autocannonautocannon -c 100 -d 30 http://localhost:3000/users-c 100 — 100 koneksi konkuren.-d 30 — selama 30 detik.Output menampilkan throughput (req/s), latency percentiles, dan jumlah error — ukuran objektif kapasitas aplikasi.
Setelah mengukur, tiga area yang paling sering di-tuning:
| Area | Parameter | Pengaruh |
|---|---|---|
| Concurrency | Jumlah instance (-i max PM2) | Throughput naik sebanding CPU |
| Database | Connection pool (max) | Hindari pool exhaustion |
| Redis | Session/cache/rate-limit shared | State terdistribusi |
Aturan mutlak tuning: satu perubahan satu pengukuran. Ganti max pool, uji lagi dengan parameter sama, bandingkan. Tanpa pengukuran berulang, tuning hanyalah perasaan.
Tip
Mulai load test dari nilai kecil (misal 20 koneksi) lalu naik bertahap sambil memperhatikan latency percentile p99. Fokus tuning adalah latency stabil di bawah target, bukan sekadar throughput maksimum — aplikasi yang sukses menekan p99 akan terasa cepat untuk semua pengguna.
Rate limit (episode 20), session (episode 12), dan Socket.IO (episode 14) semuanya butuh store bersama. Diskala tanpa Redis = proteksi dan state melenceng antar instance.
MemoryStore bukan hanya non-scalable — ia juga hilang saat restart. Tidak ada alasan memakainya di produksi.
Tanpa trust proxy, req.ip salah dan rate limit hancur saat di belakang LB. Hidupkan — dengan memahami risiko header palsu bila tidak ada proxy.
Health check yang hanya res.send("ok") tidak mendeteksi database mati. Periksa ketergantungan nyata seperti episode 24.
Episode 25 membuat aplikasi siap diskala: load balancer dengan trust proxy, session dan rate limit berbasis Redis store, Socket.IO Redis adapter, observability dengan Prometheus, serta load testing dengan autocannon dan tuning yang terukur.
Inti yang harus dibawa pulang:
trust proxy wajib diaktifkan saat di belakang load balancer.Di episode 26 selanjutnya kita akan melihat gambaran besar: ekosistem & tren modern 2026 — Express 5 vs 4, migrasi, dan perbandingan dengan Fastify, NestJS, dan Hono. Sampai jumpa di episode 26!