Node.js Rate Limiting: API Koruma Stratejileri

Node.js Rate Limiting: API Koruma Stratejileri
Rate limiting, API'nizi aşırı kullanımdan, DDoS saldırılarından ve kötü amaçlı botlardan korumanın en temel stratejilerinden biridir. Belirli bir zaman penceresi içinde yapılabilecek istek sayısını sınırlayarak sunucu kaynaklarınızı korur ve tüm kullanıcılar için adil erişim sağlar. Bu rehberde Node.js uygulamalarında rate limiting stratejilerini ve implementasyonlarını detaylı olarak inceleyeceğiz.
1. Rate Limiting Neden Gerekli?
- DDoS koruması: Sunucunun aşırı isteklerle çökertilmesini engeller
- Brute-force engelleme: Login denemelerini sınırlar
- API maliyeti kontrolü: Üçüncü parti API çağrılarını yönetir
- Adil kullanım: Tek bir kullanıcının tüm kaynakları tüketmesini önler
- Crawling/scraping engelleme: Bot trafiğini kontrol eder
2. Rate Limiting Algoritmaları
Fixed Window
Sabit zaman pencereleri içinde istek sayısını sayar. Basit ama pencere sınırlarında burst'lere açıktır.
Sliding Window
Pencereyi sürekli kaydırır, daha adil bir dağılım sağlar.
Token Bucket
Belirli hızda token eklenir, her istek bir token harcar. Burst'lere izin verirken ortalama hızı kontrol eder.
Leaky Bucket
İstekler sabit hızda işlenir, fazlası kuyrukta bekler veya reddedilir.
3. express-rate-limit ile Temel Uygulama
# Kurulum
npm install express-rate-limit
const express = require('express');
const rateLimit = require('express-rate-limit');
const app = express();
// Genel rate limiter
const generalLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 dakika
max: 100, // pencere başına 100 istek
standardHeaders: true, // RateLimit-* headers
legacyHeaders: false, // X-RateLimit-* headers kapat
message: {
status: 429,
error: 'Çok fazla istek gönderdiniz. Lütfen 15 dakika sonra tekrar deneyin.',
},
keyGenerator: (req) => {
// IP bazlı (varsayılan) veya kullanıcı bazlı
return req.user?.id || req.ip;
},
skip: (req) => {
// Belirli IP'leri veya route'ları atla
return req.ip === '127.0.0.1';
},
});
app.use('/api/', generalLimiter);
Endpoint Bazlı Rate Limiting
// Auth endpoint'leri — sıkı limit
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
message: { error: 'Çok fazla giriş denemesi. 15 dakika bekleyin.' },
skipSuccessfulRequests: true, // Başarılı istekleri sayma
});
// Arama endpoint'i — orta limit
const searchLimiter = rateLimit({
windowMs: 60 * 1000, // 1 dakika
max: 30,
message: { error: 'Çok fazla arama isteği.' },
});
// Dosya yükleme — düşük limit
const uploadLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 saat
max: 10,
message: { error: 'Dosya yükleme limiti aşıldı.' },
});
app.use('/api/auth/', authLimiter);
app.use('/api/search', searchLimiter);
app.use('/api/upload', uploadLimiter);
4. Redis ile Dağıtık Rate Limiting
Birden fazla sunucu çalıştırıyorsanız, in-memory rate limiting yeterli olmaz. Redis ile merkezi rate limiting uygulamanız gerekir.
# Kurulum
npm install rate-limit-redis ioredis
const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
const Redis = require('ioredis');
const redisClient = new Redis({
host: process.env.REDIS_HOST || 'localhost',
port: 6379,
enableOfflineQueue: false,
});
const redisLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
standardHeaders: true,
store: new RedisStore({
sendCommand: (...args) => redisClient.call(...args),
}),
});
5. Custom Rate Limiter — Token Bucket
// rateLimiter/tokenBucket.js
const Redis = require('ioredis');
const redis = new Redis();
class TokenBucketLimiter {
constructor(options) {
this.maxTokens = options.maxTokens || 10;
this.refillRate = options.refillRate || 1; // saniyede eklenen token
this.prefix = options.prefix || 'rl';
}
async consume(key, tokens = 1) {
const now = Date.now();
const bucketKey = `${this.prefix}:${key}`;
// Lua script ile atomik işlem
const script = `
local key = KEYS[1]
local maxTokens = tonumber(ARGV[1])
local refillRate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])
local bucket = redis.call('hmget', key, 'tokens', 'lastRefill')
local tokens = tonumber(bucket[1]) or maxTokens
local lastRefill = tonumber(bucket[2]) or now
-- Token yenileme
local elapsed = (now - lastRefill) / 1000
tokens = math.min(maxTokens, tokens + (elapsed * refillRate))
if tokens >= requested then
tokens = tokens - requested
redis.call('hmset', key, 'tokens', tokens, 'lastRefill', now)
redis.call('expire', key, math.ceil(maxTokens / refillRate) + 1)
return {1, math.ceil(tokens)}
else
redis.call('hmset', key, 'tokens', tokens, 'lastRefill', now)
return {0, math.ceil(tokens)}
end
`;
const [allowed, remaining] = await redis.eval(
script, 1, bucketKey,
this.maxTokens, this.refillRate, now, tokens
);
return { allowed: allowed === 1, remaining };
}
}
// Middleware olarak kullanım
function tokenBucketMiddleware(options) {
const limiter = new TokenBucketLimiter(options);
return async (req, res, next) => {
const key = req.user?.id || req.ip;
const { allowed, remaining } = await limiter.consume(key);
res.set('X-RateLimit-Remaining', remaining);
if (!allowed) {
return res.status(429).json({
error: 'Rate limit aşıldı',
remaining,
});
}
next();
};
}
app.use('/api/', tokenBucketMiddleware({
maxTokens: 100,
refillRate: 2, // saniyede 2 token
}));
6. Sliding Window Implementasyonu
// rateLimiter/slidingWindow.js
class SlidingWindowLimiter {
constructor(redis, options) {
this.redis = redis;
this.windowMs = options.windowMs || 60000;
this.max = options.max || 100;
this.prefix = options.prefix || 'sw';
}
async isAllowed(key) {
const now = Date.now();
const windowStart = now - this.windowMs;
const redisKey = `${this.prefix}:${key}`;
const pipeline = this.redis.pipeline();
pipeline.zremrangebyscore(redisKey, 0, windowStart); // Eski kayıtları sil
pipeline.zadd(redisKey, now, `${now}-${Math.random()}`); // Yeni istek ekle
pipeline.zcard(redisKey); // Toplam sayı
pipeline.expire(redisKey, Math.ceil(this.windowMs / 1000));
const results = await pipeline.exec();
const count = results[2][1];
if (count > this.max) {
// Son eklenen kaydı sil (reddedilen istek)
await this.redis.zremrangebyscore(redisKey, now, now);
return { allowed: false, remaining: 0, count };
}
return { allowed: true, remaining: this.max - count, count };
}
}
7. API Tier ve Plan Bazlı Limiting
// Plan bazlı rate limiting
const plans = {
free: { windowMs: 60 * 60 * 1000, max: 100 }, // 100/saat
basic: { windowMs: 60 * 60 * 1000, max: 1000 }, // 1000/saat
pro: { windowMs: 60 * 60 * 1000, max: 10000 }, // 10000/saat
enterprise: { windowMs: 60 * 60 * 1000, max: 100000 },
};
function planBasedLimiter(req, res, next) {
const userPlan = req.user?.plan || 'free';
const config = plans[userPlan];
const limiter = rateLimit({
...config,
keyGenerator: (req) => req.user?.apiKey || req.ip,
standardHeaders: true,
handler: (req, res) => {
res.status(429).json({
error: 'Rate limit aşıldı',
plan: userPlan,
limit: config.max,
upgrade: 'Daha yüksek limit için planınızı yükseltin.',
});
},
});
limiter(req, res, next);
}
8. Rate Limit Headers
// Standart rate limit headers
// RateLimit-Limit: 100 — Pencere başına izin verilen toplam istek
// RateLimit-Remaining: 95 — Kalan istek sayısı
// RateLimit-Reset: 1679012400 — Pencere sıfırlanma zamanı (Unix timestamp)
// Retry-After: 900 — Tekrar denemek için beklenmesi gereken saniye
// Custom header middleware
function rateLimitHeaders(req, res, next) {
const originalJson = res.json.bind(res);
res.json = function(data) {
if (res.statusCode === 429) {
res.set('Retry-After', '900');
}
return originalJson(data);
};
next();
}
app.set('trust proxy', 1) ayarını yapın. Aksi takdirde tüm istekler aynı IP'den geliyormuş gibi görünür.
Sonuç
Rate limiting, API güvenliğinin temel taşlarından biridir. Basit uygulamalar için express-rate-limit yeterli olurken, dağıtık sistemlerde Redis tabanlı çözümler gereklidir. Token bucket ve sliding window algoritmaları farklı senaryolara göre tercih edilebilir. Endpoint bazlı ve plan bazlı farklılaştırma ile hem güvenliği hem de kullanıcı deneyimini optimize edin. Rate limit header'ları ile istemcilere kalan hakları hakkında bilgi verin.