Waarom rate limiting essentieel is voor je SaaS
Stel je voor: je SaaS draait lekker, klanten gebruiken je API, en dan ineens valt alles om. Eén klant stuurt 50.000 requests per minuut — per ongeluk of expres. Zonder rate limiting ligt je platform plat voor iedereen.
Rate limiting is geen nice-to-have. Het is de basis van een stabiele, schaalbare SaaS. En als je het slim aanpakt, kun je het direct koppelen aan je prijsmodel.
De drie lagen van rate limiting
Een effectieve rate limiting strategie werkt op drie niveaus:
1. Global rate limiting (infrastructuur)
Dit is je eerste verdedigingslinie. Op nginx- of load balancer-niveau beperk je het totale aantal requests per IP.
# nginx.conf
limit_req_zone $binary_remote_addr zone=global:10m rate=100r/s;
server {
location /api/ {
limit_req zone=global burst=50 nodelay;
limit_req_status 429;
}
}
2. Per-tenant rate limiting (applicatie)
Hier wordt het interessant. Elke klant krijgt een eigen limiet, afhankelijk van hun plan.
// middleware/rateLimiter.ts
import { Redis } from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);
interface RateLimitConfig {
windowMs: number;
maxRequests: number;
}
const PLAN_LIMITS: Record<string, RateLimitConfig> = {
starter: { windowMs: 60_000, maxRequests: 100 },
growth: { windowMs: 60_000, maxRequests: 1_000 },
enterprise: { windowMs: 60_000, maxRequests: 10_000 },
};
export async function checkRateLimit(
tenantId: string,
plan: string
): Promise<{ allowed: boolean; remaining: number; resetAt: number }> {
const config = PLAN_LIMITS[plan] || PLAN_LIMITS.starter;
const windowKey = Math.floor(Date.now() / config.windowMs);
const key = `rl:${tenantId}:${windowKey}`;
const current = await redis.incr(key);
if (current === 1) {
await redis.pexpire(key, config.windowMs);
}
const remaining = Math.max(0, config.maxRequests - current);
const resetAt = (windowKey + 1) * config.windowMs;
return {
allowed: current <= config.maxRequests,
remaining,
resetAt,
};
}
3. Per-endpoint rate limiting
Sommige endpoints zijn duurder dan andere. Een GET /users is goedkoop, maar een POST /reports/generate kan minuten CPU kosten.
// Gewogen rate limiting per endpoint
const ENDPOINT_WEIGHTS: Record<string, number> = {
'GET /api/users': 1,
'GET /api/reports': 2,
'POST /api/reports': 10,
'POST /api/ai/generate': 25,
};
export function getRequestCost(method: string, path: string): number {
return ENDPOINT_WEIGHTS[`${method} ${path}`] || 1;
}
Van rate limiting naar usage-based pricing
Hier komt de echte magie: als je toch al elke API-call telt, kun je die data gebruiken voor je prijsmodel.
Het hybride model
De meeste succesvolle SaaS-bedrijven gebruiken een hybride model: een vast basisbedrag plus variabele kosten op basis van gebruik.
| Component | Starter (€49/mo) | Growth (€149/mo) | Enterprise (custom) |
|---|---|---|---|
| API calls | 10.000 incl. | 100.000 incl. | 1.000.000 incl. |
| Extra calls | €0,005/call | €0,003/call | €0,001/call |
| AI features | ❌ | 1.000 credits | Unlimited |
| Webhooks | 5 | 25 | Unlimited |
Usage tracking implementeren
// services/usageTracker.ts
import { Redis } from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);
export async function trackUsage(
tenantId: string,
metric: string,
cost: number = 1
): Promise<void> {
const monthKey = new Date().toISOString().slice(0, 7); // "2026-03"
const key = `usage:${tenantId}:${monthKey}:${metric}`;
await redis.incrbyfloat(key, cost);
// Bewaar 90 dagen
await redis.expire(key, 90 * 24 * 60 * 60);
}
export async function getUsage(
tenantId: string,
metric: string,
month?: string
): Promise<number> {
const monthKey = month || new Date().toISOString().slice(0, 7);
const key = `usage:${tenantId}:${monthKey}:${metric}`;
const value = await redis.get(key);
return parseFloat(value || '0');
}
Overage alerts: waarschuw je klanten
Niemand houdt van verrassingen op de factuur. Stuur proactieve meldingen wanneer klanten hun limiet naderen.
// services/usageAlerts.ts
const ALERT_THRESHOLDS = [0.75, 0.90, 1.0];
export async function checkUsageAlerts(
tenantId: string,
plan: string
): Promise<void> {
const usage = await getUsage(tenantId, 'api_calls');
const limit = PLAN_LIMITS[plan].monthlyIncluded;
const ratio = usage / limit;
for (const threshold of ALERT_THRESHOLDS) {
if (ratio >= threshold) {
const alertKey = `alert:${tenantId}:usage:${threshold}`;
const alreadySent = await redis.get(alertKey);
if (!alreadySent) {
await sendUsageAlert(tenantId, {
percentage: Math.round(threshold * 100),
current: usage,
limit,
});
await redis.set(alertKey, '1', 'EX', 30 * 24 * 3600);
}
}
}
}
De juiste response headers
Goede API's communiceren rate limit status via headers. Dit is de standaard:
// middleware/rateLimitHeaders.ts
export function setRateLimitHeaders(
res: Response,
result: { remaining: number; resetAt: number; limit: number }
): void {
res.setHeader('X-RateLimit-Limit', result.limit);
res.setHeader('X-RateLimit-Remaining', result.remaining);
res.setHeader('X-RateLimit-Reset', Math.ceil(result.resetAt / 1000));
res.setHeader('Retry-After', Math.ceil((result.resetAt - Date.now()) / 1000));
}
Wanneer een klant over de limiet gaat, stuur je een 429 Too Many Requests:
{
"error": "rate_limit_exceeded",
"message": "Je hebt je API-limiet bereikt. Upgrade je plan of wacht tot de reset.",
"upgrade_url": "https://app.example.com/billing/upgrade",
"reset_at": "2026-03-05T12:00:00Z"
}
Stripe-integratie voor metered billing
Als je Stripe gebruikt (zie ons eerdere artikel over Stripe-integratie), kun je usage-based pricing direct koppelen:
// services/billing.ts
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
// Dagelijks: rapporteer usage naar Stripe
export async function reportUsageToStripe(
subscriptionItemId: string,
quantity: number
): Promise<void> {
await stripe.subscriptionItems.createUsageRecord(
subscriptionItemId,
{
quantity,
timestamp: Math.floor(Date.now() / 1000),
action: 'set', // 'set' voor absolute waarde, 'increment' voor toevoeging
}
);
}
Veelgemaakte fouten
1. Te strikte limieten bij de start
Begin royaal. Niets is frustrerender voor een nieuwe klant dan direct tegen een limiet aanlopen. Je kunt altijd later verstrakken.
2. Geen grace period
Geef klanten die net over hun limiet gaan een korte grace period (bijv. 10% buffer). Hard afkappen voelt vijandig.
3. Rate limiting zonder monitoring
Als je niet weet wie er tegen limieten aanloopt, mis je upsell-kansen en bugs. Log elke 429-response.
// Log rate limit hits voor analyse
await analytics.track('rate_limit_hit', {
tenantId,
plan,
endpoint,
currentUsage: current,
limit: config.maxRequests,
});
4. Vergeten om interne services uit te sluiten
Je eigen microservices moeten niet tegen rate limits aanlopen. Gebruik een aparte authenticatie-laag voor service-to-service communicatie.
Conclusie: rate limiting als groeistrategie
Rate limiting is meer dan bescherming — het is een groeistrategie. Door het te koppelen aan usage-based pricing:
- Verlaag je de instapdrempel: klanten betalen alleen voor wat ze gebruiken
- Verhoog je de lifetime value: heavy users upgraden automatisch
- Bescherm je platform: geen enkele klant kan je service onderuit halen
- Krijg je inzicht: usage data vertelt je precies waar je product waarde levert
Begin met een simpele implementatie (Redis + sliding window), meet het gebruik, en bouw van daaruit je prijsmodel. Je klanten — en je infrastructuur — zullen je dankbaar zijn.