A Rate-Limiter-Middleware in Express bauen
A ungeschützter API-Endpoint is a Einladung: oa Client-Skript, des spinnt, oa Retry-Schleifn ohne Backoff, oa Scraper, und dein Server verbringt seine ganze Zeit damit, dieselbe Handvoll Clients zu bedienen, statt olle ondern. Rate Limiting behebt des, indem's begrenzt, wie vül Requests a Client in oam bestimmten Fenster machn darf - und du kannst a solides in etwa 40 Zeiln söba bauen, ohne Abhängigkeit.
Wos "Rate Limiting" tatsächlich heißt
A Rate Limiter trackt Requests pro Client (meist über IP-Adresse oda API-Key) und lehnt Requests ab, sobois a Client innerhoib vom Zeitfenster a Schwelle überschreitet - zum Beispiel "100 Requests pro Minute". Sobois das Fenster vorbei is, wird da Zähler zruckgsetzt und da Client darf wieda Requests machn.
Die einfachste korrekte Implementierung is der Fixed-Window-Counter: Pro Client hoitst an Zähler und an Fenster-Start-Zeitstempel. Bei jedem Request erhöhn; beides zruckstelln, wenn das Fenster abläuft.
A minimaler Fixed-Window-Limiter
function createRateLimiter({ windowMs, max }) {
const clients = new Map();
return function rateLimiter(req, res, next) {
const key = req.ip;
const now = Date.now();
const entry = clients.get(key);
if (!entry || now - entry.windowStart >= windowMs) {
clients.set(key, { windowStart: now, count: 1 });
return next();
}
if (entry.count >= max) {
const retryAfterMs = windowMs - (now - entry.windowStart);
res.setHeader('Retry-After', Math.ceil(retryAfterMs / 1000));
return res.status(429).json({ error: 'Too many requests' });
}
entry.count += 1;
next();
};
}
Einbindung in a Express-App:
const express = require('express');
const app = express();
const limiter = createRateLimiter({ windowMs: 60_000, max: 100 });
app.use('/api/', limiter);
app.get('/api/posts', (req, res) => {
res.json({ posts: [] });
});
app.listen(3000);
Jeder Client kriagt 100 Requests pro 60-Sekunden-Fenster, bevor er a 429 Too Many Requests sieht, mit am Retry-After-Header, der ihm sagt, wann er wiederkemman soi.
Das Fixed-Window-Problem: Boundary-Bursts
Fixed Windows hom an bekannten Makel: A Client kann max Requests in da letztn Sekunde von oam Fenster machn und dann sofort max weitere in da ersten Sekunde vom nächsten - 2x die beabsichtigte Rate, konzentriert in oam Zwoa-Sekunden-Burst direkt an da Grenze.
window 1: [ ......................############] <- 100 requests, last second
window 2: [############......................] <- 100 more, first second
Für de meisten APIs spielt des ka große Rolle. Wenn doch, ist die Lösung a Sliding Window Log: Statt oam Zähler pro Client speicherst den Zeitstempel von jedem Request im Fenster und zählst, wie vül in now - windowMs fallen.
function createSlidingWindowLimiter({ windowMs, max }) {
const clients = new Map();
return function rateLimiter(req, res, next) {
const key = req.ip;
const now = Date.now();
const timestamps = clients.get(key) ?? [];
const windowStart = now - windowMs;
const recent = timestamps.filter((t) => t > windowStart);
if (recent.length >= max) {
clients.set(key, recent);
res.setHeader('Retry-After', Math.ceil((recent[0] - windowStart) / 1000));
return res.status(429).json({ error: 'Too many requests' });
}
recent.push(now);
clients.set(key, recent);
next();
};
}
Des is exakt, kost aber mehr Speicher (oan Zeitstempel pro Request pro Client, ned oan Zähler). Für APIs mit viel Traffic kommt a Hybrid - der Sliding Window Counter, der den Zähler vom vorherigen Fenster danach gewichtet, wie stark er das aktuelle überlappt - fast an exaktes Verhalten bei den Speicherkosten von am Fixed Window. Es is mehr Mathematik ois Code, drum greifen de meisten Teams zu ana Bibliothek oda zu am Limiter auf Proxy-Ebene (nginx' limit_req oda oan mit Redis dahinter), sobois sie des im großen Stil brauchen.
An Rate-Limiting-Key wählen
req.ip is der offensichtliche Key, aber er bricht hinter am geteilten NAT (a Büro voller Entwickler, de sich oane öffentliche IP teiln) oda am Proxy, der die echte Client-Adresse ned weiterleitet. Zwoa Anpassungen zählen in da Praxis:
- Trau deim Proxy richtig. Hinter nginx oda am Load Balancer is
req.ipdie Adresse vom Proxy, außer du konfigurierst Express, ihm zu vertrauen:app.set('trust proxy', 1)undreq.ipausX-Forwarded-Forlesn. Machst des falsch, wird jeder Client wia oaner limitiert. - Bevorzug an API-Key gegenüber ana IP, wo du oan hast. Authentifizierte Endpoints soin pro API-Key oda User-ID limitiern, ned pro IP - des is die tatsächliche Identität, um de's dir geht, und sie überlebt IP-Wechsel und geteilte Netzwerke.
Veraltete Einträge aufräumen
Die Map in beiden Implementierungen oben wachst ewig, außer du verdrängst alte Einträge - a langsames Speicherleck in am langlaufenden Prozess. A einfacher periodischer Sweep erledigt des:
setInterval(() => {
const cutoff = Date.now() - windowMs;
for (const [key, entry] of clients) {
if (entry.windowStart < cutoff) clients.delete(key);
}
}, windowMs).unref();
Das .unref() zählt in Node: Es sagt dem Event Loop, den Prozess ned nur wegn dem Timer am Leben zum hoitn, damit's ned mit dem sauberen Herunterfahren kollidiert.
Wann ma stattdessen zu ana Bibliothek greift
express-rate-limit implementiert olles Obige (plus IPv6-Behandlung und austauschbare Stores) und is der richtige Standard für a Single-Process-App. Sobois du mehrere Instanzen hinter am Load Balancer fahrst, funktioniert a In-Memory-Map nimma - jede Instanz hot ihre eigenen Zähler, also kann a Client max Requests pro Instanz kriagn statt max insgesamt. Do brauchst an geteilten Store, und express-rate-limit unterstützt genau deswegn oan mit Redis dahinter.
Fazit
A Rate Limiter is grundsätzlich a Zähler mit am Zeitfenster dran - olles andere is Verfeinerung. Fang mit am Fixed Window o; es is einfach, billig und für de meisten APIs korrekt gnua. Wechsl erst auf a Sliding Window, wenn Boundary-Bursts a tatsächliches, gmessenes Problem sand, und wechsl auf an geteilten Store in dem Moment, wo du mehr ois oan Server-Prozess fahrst.