Redis Adalah: Panduan Pemula + Caching Node.js (Express) Terintegrasi (ETag, Rate Limit, Session, SWR)
Redis adalah open-source in-memory data structure store yang digunakan sebagai database, cache, dan message broker. Karena bekerja di memori, Redis mampu memberikan akses data sangat cepat dan sering dipakai untuk meningkatkan performa aplikasi Node.js, khususnya pada kebutuhan caching, session store, dan rate limiting.
Di artikel ini Anda akan mendapatkan versi lengkap dan terintegrasi (bukan potongan terpisah) tentang:
- Apa itu Redis dan kenapa populer di Node.js
- Caching API response dengan Redis
- Caching per-route dengan ETag + conditional request (304 Not Modified)
- Rate limiting dengan Redis (per IP + per route)
- Session store Express menggunakan Redis (connect-redis)
- Pola stale-while-revalidate untuk mencegah cache stampede
- Kode final: 1 file server.js siap jalan
Apa Itu Redis?

Redis adalah sistem penyimpanan data yang beroperasi di memori (RAM). Redis menyimpan data dalam format key-value dan mendukung berbagai struktur data seperti:
- String
- List
- Hash
- Set
- Sorted Set
Redis sangat cocok untuk skenario yang membutuhkan akses data super cepat, misalnya:
- caching respons API
- session store login
- leaderboard game
- sistem antrian (queue)
- rate limiting
- analitik waktu nyata
Kenapa Redis Cocok untuk Caching?
Caching adalah proses menyimpan data yang sering diminta agar permintaan berikutnya tidak perlu menghitung ulang atau memanggil API/database lagi.
Redis cocok untuk caching karena:
- operasi GET/SET cepat (in-memory)
- punya TTL bawaan (expired otomatis)
- mudah dipakai untuk key prefix (misalnya
jobs:*) - mendukung pola anti stampede seperti SWR
Alur Caching yang Akan Kita Bangun
Untuk endpoint GET /jobs?search=node:
- Buat key cache:
jobs:node - Cek Redis:
- ada → HIT → balas dari cache
- tidak ada → MISS → fetch API eksternal → simpan ke Redis
- Hitung dan kirim ETag
- Jika client kirim
If-None-Match, server bisa balas 304 Not Modified - Terapkan rate limit agar endpoint tidak diserang
- Terapkan session store agar scalable
- Terapkan stale-while-revalidate agar tidak terjadi stampede
Instal Redis (Cara Cepat)
Opsi Docker (paling cepat)
docker run -d --name redis -p 6379:6379 redis:7-alpine
Cek:
redis-cli ping
# PONG
Setup Project Node.js

Buat folder dan install dependensi:
mkdir redis-node && cd redis-node
npm init -y
npm i express axios redis etag express-session connect-redis
KODE FINAL TERINTEGRASI (1 FILE) — server.js
Kode berikut sudah berisi:
✅ Redis cache
✅ ETag + 304
✅ rate limiting Redis
✅ session store Redis
✅ stale-while-revalidate + lock (anti stampede)
✅ fallback jika Redis down (server tetap jalan)
Buat file server.js:
'use strict';
const express = require('express');
const axios = require('axios');
const etag = require('etag');
const session = require('express-session');
const RedisStore = require('connect-redis').default;
const { createClient } = require('redis');
const app = express();
// =========================
// Konfigurasi ENV
// =========================
const PORT = process.env.PORT || 3000;
const REDIS_URL = process.env.REDIS_URL || 'redis://localhost:6379';
const SESSION_SECRET = process.env.SESSION_SECRET || 'ganti-secret-anda';
const HTTP_TIMEOUT_MS = Number(process.env.HTTP_TIMEOUT_MS || 8000);
// Cache SWR
const SOFT_TTL_SECONDS = Number(process.env.SOFT_TTL_SECONDS || 300); // fresh 5 menit
const HARD_TTL_SECONDS = Number(process.env.HARD_TTL_SECONDS || 1800); // stale allowed 30 menit
// Rate limit
const RATE_LIMIT = Number(process.env.RATE_LIMIT || 60); // 60 req
const RATE_WINDOW_SECONDS = Number(process.env.RATE_WINDOW_SECONDS || 60); // per 60 detik
// =========================
// Redis Client (node-redis v4)
// =========================
const redis = createClient({ url: REDIS_URL });
redis.on('error', (err) => {
console.error('[Redis error]', err?.message || err);
});
async function initRedis() {
if (!redis.isOpen) {
await redis.connect();
console.log('[Redis] connected');
}
}
// =========================
// Express Session (Redis store)
// =========================
app.set('trust proxy', 1); // penting jika deploy di reverse proxy (Nginx, Cloudflare, dsb)
app.use(
session({
store: new RedisStore({
client: redis,
prefix: 'sess:',
}),
secret: SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
// secure: true, // aktifkan jika sudah HTTPS
maxAge: 1000 * 60 * 60 * 2, // 2 jam
},
})
);
// =========================
// Helper Key Builder
// =========================
function normalizeSearch(search) {
return (search || '').trim().toLowerCase();
}
function buildJobsCacheKey(search) {
return `jobs:${normalizeSearch(search) || 'all'}`;
}
// =========================
// Rate Limit Middleware Redis
// =========================
function rateLimitRedis({ limit, windowSeconds, keyPrefix = 'rl' }) {
return async function (req, res, next) {
if (!redis.isOpen) return next();
try {
const ip = (req.headers['x-forwarded-for']?.split(',')[0] || req.ip || 'unknown').trim();
const route = req.baseUrl + req.path;
const windowKey = Math.floor(Date.now() / (windowSeconds * 1000));
const key = `${keyPrefix}:${ip}:${route}:${windowKey}`;
const count = await redis.incr(key);
if (count === 1) {
await redis.expire(key, windowSeconds);
}
res.set('X-RateLimit-Limit', String(limit));
res.set('X-RateLimit-Remaining', String(Math.max(0, limit - count)));
if (count > limit) {
return res.status(429).json({
error: 'Too Many Requests',
message: 'Rate limit exceeded. Please try again later.',
});
}
return next();
} catch (err) {
console.error('[rateLimitRedis]', err?.message || err);
return next();
}
};
}
// =========================
// Fetch Upstream API
// =========================
// NOTE: GitHub Jobs API sudah deprecated.
// Kita pakai Remotive API yang masih aktif.
async function fetchJobsFromRemotive(search) {
const url = 'https://remotive.com/api/remote-jobs';
const resp = await axios.get(url, {
timeout: HTTP_TIMEOUT_MS,
params: search ? { search } : {},
headers: { Accept: 'application/json' },
});
return Array.isArray(resp.data?.jobs) ? resp.data.jobs : [];
}
// =========================
// Stale-While-Revalidate (SWR)
// =========================
async function setSWR({ key, metaKey, softTtlSeconds, hardTtlSeconds, payload }) {
const now = Date.now();
const meta = {
freshUntil: now + softTtlSeconds * 1000,
createdAt: now,
};
await redis.setEx(key, hardTtlSeconds, payload);
await redis.setEx(metaKey, hardTtlSeconds, JSON.stringify(meta));
}
async function triggerRevalidate({ key, metaKey, lockKey, softTtlSeconds, hardTtlSeconds, fetcher }) {
if (!redis.isOpen) return;
// lock 15 detik agar hanya 1 worker refresh
const acquired = await redis.set(lockKey, '1', { NX: true, EX: 15 });
if (!acquired) return;
try {
const newPayload = await fetcher();
await setSWR({ key, metaKey, softTtlSeconds, hardTtlSeconds, payload: newPayload });
} catch (err) {
console.error('[SWR revalidate]', err?.message || err);
} finally {
await redis.del(lockKey).catch(() => {});
}
}
async function getSWR({ key, softTtlSeconds, hardTtlSeconds, fetcher }) {
const metaKey = `${key}:meta`;
const lockKey = `${key}:lock`;
if (!redis.isOpen) {
// Redis down -> fallback langsung fetch
const payload = await fetcher();
return { payload, state: 'miss' };
}
const [payload, metaRaw] = await redis.mGet([key, metaKey]);
// Jika cache ada
if (payload && metaRaw) {
const meta = JSON.parse(metaRaw);
const now = Date.now();
// Masih fresh
if (now <= meta.freshUntil) {
return { payload, state: 'fresh' };
}
// Stale -> balas payload cepat, refresh background
triggerRevalidate({ key, metaKey, lockKey, softTtlSeconds, hardTtlSeconds, fetcher }).catch(() => {});
return { payload, state: 'stale' };
}
// Cache miss total -> fetch sekarang
const newPayload = await fetcher();
await setSWR({ key, metaKey, softTtlSeconds, hardTtlSeconds, payload: newPayload });
return { payload: newPayload, state: 'miss' };
}
// =========================
// Routes
// =========================
// Health check
app.get('/health', (req, res) => {
res.json({
status: 'ok',
redis: redis.isOpen ? 'connected' : 'disconnected',
session: req.sessionID || null,
time: new Date().toISOString(),
});
});
// Demo session
app.get('/login-mock', (req, res) => {
req.session.userId = 'user_123';
res.json({ ok: true, sessionId: req.sessionID, userId: req.session.userId });
});
app.get('/me', (req, res) => {
res.json({ userId: req.session.userId || null });
});
// Endpoint utama: /jobs
app.get(
'/jobs',
rateLimitRedis({ limit: RATE_LIMIT, windowSeconds: RATE_WINDOW_SECONDS }),
async (req, res) => {
const search = req.query.search ? String(req.query.search) : '';
const key = buildJobsCacheKey(search);
try {
const { payload, state } = await getSWR({
key,
softTtlSeconds: SOFT_TTL_SECONDS,
hardTtlSeconds: HARD_TTL_SECONDS,
fetcher: async () => {
const jobs = await fetchJobsFromRemotive(search);
return JSON.stringify({
source: 'remotive',
query: search || null,
count: jobs.length,
jobs,
generatedAt: new Date().toISOString(),
});
},
});
// ETag untuk conditional request
const responseEtag = etag(payload);
res.set('ETag', responseEtag);
// Jika client kirim If-None-Match -> 304
const ifNoneMatch = req.headers['if-none-match'];
if (ifNoneMatch && ifNoneMatch === responseEtag) {
res.set('X-Cache', state.toUpperCase());
return res.status(304).end();
}
res.set('X-Cache', state.toUpperCase()); // FRESH / STALE / MISS
res.set('Content-Type', 'application/json; charset=utf-8');
return res.status(200).send(payload);
} catch (err) {
const message = err?.response?.data || err?.message || 'Unknown error';
return res.status(502).json({ error: 'Upstream API error', message });
}
}
);
// =========================
// Start Server
// =========================
(async () => {
try {
await initRedis();
} catch (e) {
console.error('[Redis] failed to connect, continuing without cache:', e?.message || e);
}
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
console.log(`Try: http://localhost:${PORT}/jobs?search=node`);
});
})();
Cara Menjalankan
node server.js
Uji:
- Request pertama (MISS)
curl -i "http://localhost:3000/jobs?search=node"
- Request kedua (FRESH/HIT)
curl -i "http://localhost:3000/jobs?search=node"
- Uji conditional request (304)
- Ambil
ETagdari response pertama - Jalankan:
curl -i "http://localhost:3000/jobs?search=node" -H 'If-None-Match: "<ETAG>"'
Penjelasan Ringkas Fitur Terintegrasi

1) ETag + Conditional Request
ETag membuat browser/proxy tidak perlu download ulang payload jika sama. Server cukup balas 304.
2) Rate Limiting Redis
Mencegah abuse endpoint. Cocok untuk API publik, scraping, atau serangan request spam.
3) Session Store Redis
Session tidak disimpan di memory server, sehingga aplikasi tetap aman saat:
- restart
- multi server (load balancer)
- scaling horizontal
4) Stale-While-Revalidate
Mencegah cache stampede:
- request tetap cepat meski cache stale
- hanya 1 request melakukan refresh (lock)
Kesimpulan
Redis adalah teknologi penting dalam sistem backend modern, terutama untuk Node.js. Dengan Redis Anda bisa membangun sistem caching yang cepat, scalable, dan stabil. Versi integrasi yang benar bukan hanya GET/SET biasa, tetapi juga perlu fitur tambahan seperti ETag, rate limiting, session store, dan stale-while-revalidate agar aplikasi siap untuk trafik nyata.



