{"id":117,"date":"2026-06-17T16:21:09","date_gmt":"2026-06-17T16:21:09","guid":{"rendered":"https:\/\/shattered.io\/at\/2026\/06\/17\/rate-limiting-nodejs\/"},"modified":"2026-06-17T16:21:09","modified_gmt":"2026-06-17T16:21:09","slug":"rate-limiting-nodejs","status":"publish","type":"post","link":"https:\/\/shattered.io\/at\/rate-limiting-nodejs\/","title":{"rendered":"Rate Limiting in Node.js: 12 Schritte, 35 Min [2026]"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Rate Limiting in Node.js sch\u00fctzt deine API vor Brute-Force-Angriffen, DDoS-Wellen und automatisierten Scrapers. Wer keinen Anfrage-Begrenzer einsetzt, riskiert Serverausf\u00e4lle, gestohlene Zugangsdaten und hohe Cloud-Rechnungen. Dieses Tutorial zeigt dir in 12 konkreten Schritten, wie du mit <strong>express-rate-limit 8.5.2<\/strong> und <strong>rate-limiter-flexible 11.2.0<\/strong> einen produktionsreifen Schutz aufbaust. Arbeitszeit: rund 35 Minuten.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"was-ist-rate-limiting-und-warum-brauchst-du-es-2026\">Was ist Rate Limiting und warum brauchst du es 2026?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Rate Limiting begrenzt, wie viele Anfragen ein Client (identifiziert \u00fcber IP-Adresse, API-Key oder User-ID) in einem definierten Zeitfenster an deinen Server schicken darf. \u00dcberschreitet er das Limit, antwortet der Server mit dem HTTP-Statuscode <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/HTTP\/Reference\/Status\/429\" rel=\"noopener\" target=\"_blank\">429 Too Many Requests<\/a>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Der <a href=\"\/at\/cyberangriffe-dach-2026\/\">DACH-Raum verzeichnet 2026 w\u00f6chentlich \u00fcber 2.122 Cyberangriffe allein in \u00d6sterreich<\/a>. Brute-Force-Angriffe auf Login-Endpunkte geh\u00f6ren laut CERT.at zu den h\u00e4ufigsten Angriffsmustern. Ohne Rate Limiting kann ein Angreifer in einer Minute Tausende Passwort-Kombinationen durchprobieren. Das <a href=\"https:\/\/owasp.org\/API-Security\/editions\/2023\/en\/0xa4-unrestricted-resource-consumption\/\" rel=\"noopener\" target=\"_blank\">OWASP API Security Top 10 (2023) listet unter API4:2023 &#8220;Unrestricted Resource Consumption&#8221;<\/a> explizit das Fehlen von Rate Limiting als kritische Schwachstelle.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Konkrete Zahlen aus der Praxis: Eine Node.js-API ohne Rate Limiting kann auf typischer Server-Hardware mehrere Tausend Anfragen pro Sekunde verarbeiten. Das bedeutet, ein einzelner Angreifer mit einem einfachen Skript kann in 15 Minuten \u00fcber 1 Million Passwort-Kombinationen testen. Mit Rate Limiting auf 10 Versuche pro 15 Minuten reduziert sich das auf gerade 10 Kombinationen, was einen Angriff wirtschaftlich sinnlos macht.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Angriffstyp<\/th><th>Ohne Rate Limiting<\/th><th>Mit Rate Limiting<\/th><\/tr><\/thead><tbody><tr><td>Brute-Force-Login<\/td><td>Unbegrenzte Versuche pro Sekunde<\/td><td>Max. 5 Versuche \/ 15 Min. pro IP<\/td><\/tr><tr><td>API-Scraping<\/td><td>Komplette Datenbank abrufbar<\/td><td>100 Anfragen \/ 15 Min. geblockt<\/td><\/tr><tr><td>DDoS-Welle<\/td><td>Serverausfall in Sekunden<\/td><td>Verkehr gedrosselt, Server stabil<\/td><\/tr><tr><td>Credential Stuffing<\/td><td>1.000 Accounts\/Min. pr\u00fcfbar<\/td><td>Automatisch nach 10 Versuchen gesperrt<\/td><\/tr><tr><td>Kostenexplosion (Cloud)<\/td><td>Unbegrenzte Serverkosten<\/td><td>Anfragen gedeckelt, Kosten kontrolliert<\/td><\/tr><tr><td>E-Mail-Flooding<\/td><td>Unbegrenzte Reset-Mails versendbar<\/td><td>Max. 3 Reset-Mails \/ Stunde pro Konto<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"die-drei-rate-limiting-algorithmen-im-vergleich\">Die drei Rate-Limiting-Algorithmen im Vergleich<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Bevor du Code schreibst, musst du den richtigen Algorithmus w\u00e4hlen. Die Wahl beeinflusst Speicherverbrauch, Genauigkeit und Burst-Toleranz deiner API. Kein Algorithmus ist universell optimal; die Anforderungen deiner Anwendung bestimmen die richtige Wahl.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"fixed-window-festes-zeitfenster\">Fixed Window (Festes Zeitfenster)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Das einfachste Modell: Innerhalb eines festen Fensters (z.B. jede Minute von 00 bis 59 Sekunden) darf ein Client maximal <em>N<\/em> Anfragen stellen. Der Z\u00e4hler wird am Fensterrand zur\u00fcckgesetzt. Das Problem: An der Grenze zweier Fenster kann ein Client kurzzeitig die doppelte Anzahl Anfragen senden, ohne das Limit zu verletzen. Geeignet f\u00fcr unkritische Endpunkte und geringe Last. Wenig Speicherverbrauch, einfache Implementierung.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"sliding-window-gleitendes-zeitfenster\">Sliding Window (Gleitendes Zeitfenster)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Statt eines starren Resets wird der Z\u00e4hler kontinuierlich gegen ein r\u00fcckw\u00e4rtslaufendes Zeitfenster gemessen. Macht ein Client um 12:00:30 Uhr seine 100. Anfrage, z\u00e4hlt das Fenster von 11:59:30 bis 12:00:30. Grenzeffekte entfallen. Speicherverbrauch ist h\u00f6her, weil Zeitstempel einzelner Anfragen gespeichert werden. Die <strong>Sliding Counter<\/strong>-Variante approximiert das mit weniger Speicher bei guter Genauigkeit.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"token-bucket-token-eimer\">Token Bucket (Token-Eimer)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Stell dir einen Eimer vor, der sich mit einer festen Rate mit Tokens f\u00fcllt. Jede Anfrage kostet einen Token. Ist der Eimer leer, wird die Anfrage abgelehnt. F\u00fcllt er sich schneller als Anfragen kommen, bildet sich ein Vorrat, der kurze Traffic-Bursts erlaubt. Das ist das bevorzugte Modell f\u00fcr produktive APIs, die organischen Nutzer-Traffic mit gelegentlichen Spitzen bedienen. <a href=\"https:\/\/oneuptime.com\/blog\/post\/2026-01-06-nodejs-rate-limiting-no-external-services\/view\" rel=\"noopener\" target=\"_blank\">Laut einem aktuellen Node.js-Implementierungsguide<\/a> bietet Token Bucket die beste Balance aus Burst-Toleranz und Speichereffizienz.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Algorithmus<\/th><th>Speicher<\/th><th>Genauigkeit<\/th><th>Burst-Toleranz<\/th><th>Empfohlen f\u00fcr<\/th><\/tr><\/thead><tbody><tr><td>Fixed Window<\/td><td>Sehr gering<\/td><td>Gering<\/td><td>Gering (2x an Grenzen)<\/td><td>Interne Tools, geringe Last<\/td><\/tr><tr><td>Sliding Window Log<\/td><td>Hoch<\/td><td>Sehr hoch<\/td><td>Pr\u00e4zise<\/td><td>Finanzielle APIs, Compliance<\/td><\/tr><tr><td>Sliding Counter<\/td><td>Gering<\/td><td>Hoch<\/td><td>Glatt<\/td><td>User-facing APIs<\/td><\/tr><tr><td>Token Bucket<\/td><td>Gering<\/td><td>Mittel<\/td><td>Hoch (kontrolliert)<\/td><td>Public APIs, OAuth-Endpunkte<\/td><\/tr><tr><td>Leaky Bucket<\/td><td>Gering<\/td><td>Mittel<\/td><td>Keine (gleichm\u00e4\u00dfig)<\/td><td>Streaming, Warteschlangen<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"voraussetzungen\">Voraussetzungen<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Bevor du startest, stelle sicher, dass folgende Software installiert ist und die angegebenen Versionen oder neuere verf\u00fcgbar sind:<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li><strong>Node.js 22.x LTS<\/strong> oder h\u00f6her (Pr\u00fcfen: <code>node --version<\/code>)<\/li><li><strong>npm 10.x<\/strong> oder h\u00f6her (Pr\u00fcfen: <code>npm --version<\/code>)<\/li><li><strong>Express.js 4.21.x<\/strong> oder 5.x<\/li><li><strong>Redis 7.x<\/strong> (optional, f\u00fcr verteilte Systeme; Pr\u00fcfen: <code>redis-server --version<\/code>)<\/li><li>Ein Terminalprogramm (Terminal, PowerShell oder WSL2 unter Windows)<\/li><li>Grundkenntnisse in JavaScript und REST-APIs<\/li><\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Falls du Node.js noch nicht installiert hast, lade die LTS-Version von nodejs.org herunter. Redis kannst du unter Ubuntu mit <code>sudo apt install redis-server<\/code> installieren, unter macOS mit <code>brew install redis<\/code>. F\u00fcr Windows empfiehlt sich Redis unter WSL2.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"schritt-1-projekt-aufsetzen\">Schritt 1: Projekt aufsetzen<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Erstelle ein neues Verzeichnis und initialisiere ein Node.js-Projekt. Wir bauen eine einfache Express-API mit mehreren Routen, die wir schrittweise absichern. Diese Basis dient als vollst\u00e4ndiges Demoprojekt, das du am Ende des Tutorials direkt in Produktion einsetzen kannst.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>mkdir nodejs-rate-limit-demo\ncd nodejs-rate-limit-demo\nnpm init -y\nnpm install express<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Erstelle eine Basisdatei <code>index.js<\/code> mit folgendem Inhalt:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const express = require('express');\nconst app = express();\nconst PORT = process.env.PORT || 3000;\n\napp.use(express.json());\n\n\/\/ \u00d6ffentliche Route (kein Auth erforderlich)\napp.get('\/api\/produkte', (req, res) => {\n  res.json({ produkte: ['Laptop', 'Maus', 'Tastatur'] });\n});\n\n\/\/ Login-Endpunkt (besonders sch\u00fctzenswert)\napp.post('\/api\/login', (req, res) => {\n  const { email, passwort } = req.body;\n  if (email === 'nutzer@beispiel.at' && passwort === 'geheim123') {\n    res.json({ token: 'jwt-token-hier' });\n  } else {\n    res.status(401).json({ fehler: 'Ung\u00fcltige Zugangsdaten' });\n  }\n});\n\n\/\/ Passwort zur\u00fccksetzen\napp.post('\/api\/passwort-reset', (req, res) => {\n  res.json({ nachricht: 'Reset-E-Mail wurde gesendet' });\n});\n\n\/\/ F\u00fcr Tests: App exportieren, nicht direkt starten\nif (require.main === module) {\n  app.listen(PORT, () => {\n    console.log(`Server l\u00e4uft auf http:\/\/localhost:${PORT}`);\n  });\n}\n\nmodule.exports = app;<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Starte den Server mit <code>node index.js<\/code> und \u00fcberpr\u00fcfe mit <code>curl http:\/\/localhost:3000\/api\/produkte<\/code>, ob er antwortet. Erwartet wird: <code>{\"produkte\":[\"Laptop\",\"Maus\",\"Tastatur\"]}<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"schritt-2-express-rate-limit-installieren\">Schritt 2: express-rate-limit installieren<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>express-rate-limit<\/strong> ist das meistgenutzte Rate-Limiting-Middleware-Paket f\u00fcr Express.js. Version 8.5.2 (Stand Juni 2026) unterst\u00fctzt Node.js 16 und h\u00f6her und setzt standardm\u00e4\u00dfig auf die <code>RateLimit-*<\/code>-Header nach IETF Draft f\u00fcr HTTP Rate Limit Headers. Das Paket hat weit \u00fcber 10 Millionen w\u00f6chentliche Downloads auf npm und ist in fast jedem produktiven Node.js-Sicherheits-Stack zu finden.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>npm install express-rate-limit@8.5.2<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u00dcberpr\u00fcfe die Installation:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>npm list express-rate-limit\n# Erwartet:\n# nodejs-rate-limit-demo@1.0.0\n# \u2514\u2500\u2500 express-rate-limit@8.5.2<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">F\u00fcr TypeScript-Projekte sind die Typdefinitionen direkt im Paket enthalten, kein separates <code>@types\/<\/code>-Paket n\u00f6tig.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"schritt-3-globales-rate-limiting-konfigurieren\">Schritt 3: Globales Rate Limiting konfigurieren<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Das globale Limit greift f\u00fcr alle Routen deiner API. Es ist die erste Verteidigungslinie gegen allgemeines API-Flooding und verhindert, dass ein einzelner Client die gesamte Serverkapazit\u00e4t monopolisiert. F\u00fcge folgenden Code in <code>index.js<\/code> ein, direkt nach den <code>require<\/code>-Anweisungen und der <code>app<\/code>-Initialisierung:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const rateLimit = require('express-rate-limit');\n\n\/\/ Globales Limit: 100 Anfragen pro 15 Minuten pro IP\nconst globalLimiter = rateLimit({\n  windowMs: 15 * 60 * 1000,   \/\/ 15 Minuten in Millisekunden\n  max: 100,                    \/\/ Maximale Anfragen pro Fenster\n  standardHeaders: 'draft-8', \/\/ RateLimit-*-Header nach IETF-Draft\n  legacyHeaders: false,        \/\/ X-RateLimit-*-Header deaktivieren\n  message: {\n    fehler: 'Zu viele Anfragen. Bitte warte 15 Minuten.',\n    retryAfter: '15 Minuten'\n  }\n});\n\n\/\/ Globales Limit auf alle Routen anwenden\napp.use(globalLimiter);<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Teste das Limit mit einem einfachen Bash-Skript, das 105 Anfragen in schneller Folge sendet:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>for i in $(seq 1 105); do\n  STATUS=$(curl -s -o \/dev\/null -w \"%{http_code}\" http:\/\/localhost:3000\/api\/produkte)\n  echo \"Anfrage $i: HTTP $STATUS\"\ndone<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Ab Anfrage 101 erscheint folgende Ausgabe:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Anfrage 98: HTTP 200\nAnfrage 99: HTTP 200\nAnfrage 100: HTTP 200\nAnfrage 101: HTTP 429\nAnfrage 102: HTTP 429\nAnfrage 103: HTTP 429<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Die Antwort bei HTTP 429 enth\u00e4lt automatisch den <code>Retry-After<\/code>-Header, der dem Client mitteilt, wann er es wieder versuchen darf. Gut implementierte API-Clients lesen diesen Header aus und warten entsprechend, statt sofort erneut zu versuchen.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"schritt-4-routen-spezifisches-rate-limiting\">Schritt 4: Routen-spezifisches Rate Limiting<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Nicht alle Endpunkte brauchen dasselbe Limit. Ein Login-Formular sollte viel strenger begrenzt werden als ein \u00f6ffentlicher Produktkatalog. <a href=\"https:\/\/coreui.io\/answers\/how-to-implement-rate-limiting-in-nodejs\/\" rel=\"noopener\" target=\"_blank\">Best Practices f\u00fcr 2026<\/a> empfehlen, Auth-Endpunkte 10-mal strenger zu begrenzen als regul\u00e4re API-Endpunkte. Erstelle separate Limiter-Instanzen f\u00fcr verschiedene Sensibilit\u00e4tsstufen:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ Striktes Limit f\u00fcr Auth-Endpunkte: 10 Versuche \/ 15 Minuten\nconst authLimiter = rateLimit({\n  windowMs: 15 * 60 * 1000,\n  max: 10,\n  standardHeaders: 'draft-8',\n  legacyHeaders: false,\n  skipSuccessfulRequests: true, \/\/ Erfolgreiche Logins z\u00e4hlen nicht mit\n  message: {\n    fehler: 'Zu viele Login-Versuche. Warte 15 Minuten.',\n    code: 'AUTH_RATE_LIMIT'\n  }\n});\n\n\/\/ Moderates Limit f\u00fcr schreibende Operationen: 30 \/ 10 Minuten\nconst writeLimiter = rateLimit({\n  windowMs: 10 * 60 * 1000,\n  max: 30,\n  standardHeaders: 'draft-8',\n  legacyHeaders: false\n});\n\n\/\/ Auth-Routen mit striktem Limiter absichern\napp.post('\/api\/login', authLimiter, (req, res) => {\n  const { email, passwort } = req.body;\n  if (email === 'nutzer@beispiel.at' && passwort === 'geheim123') {\n    res.json({ token: 'jwt-token-hier' });\n  } else {\n    res.status(401).json({ fehler: 'Ung\u00fcltige Zugangsdaten' });\n  }\n});\n\napp.post('\/api\/passwort-reset', authLimiter, (req, res) => {\n  res.json({ nachricht: 'Reset-E-Mail wurde gesendet' });\n});\n\n\/\/ Schreibende Routen mit moderatem Limiter\napp.post('\/api\/bewertung', writeLimiter, (req, res) => {\n  res.json({ nachricht: 'Bewertung gespeichert' });\n});<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Die Option <code>skipSuccessfulRequests: true<\/code> ist besonders f\u00fcr Login-Endpunkte sinnvoll: Erfolgreiche Anmeldungen (HTTP 2xx) z\u00e4hlen nicht gegen das Limit. So werden echte Nutzer nicht ausgesperrt, w\u00e4hrend Brute-Force-Angreifer schnell das Limit erreichen, da deren Versuche fast immer scheitern.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"schritt-5-ip-erkennung-hinter-einem-reverse-proxy-korrekt-konfigurieren\">Schritt 5: IP-Erkennung hinter einem Reverse Proxy korrekt konfigurieren<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Einer der h\u00e4ufigsten Fehler beim Rate Limiting: Wenn deine Node.js-App hinter nginx, einem AWS Load Balancer oder Cloudflare sitzt, sieht <code>req.ip<\/code> immer die IP des Proxys, nicht des echten Clients. Das Rate Limit gilt dann f\u00fcr alle Nutzer gemeinsam, und das System sperrt sich buchst\u00e4blich selbst.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Konfiguriere Express so, dass es dem <code>X-Forwarded-For<\/code>-Header vertraut:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ Direkt nach app = express():\n\/\/ Anzahl der vertrauensw\u00fcrdigen Proxy-Hops setzen\n\/\/ 1 = nginx auf demselben Server\n\/\/ 2 = nginx + AWS ELB\n\/\/ true = allen Proxys vertrauen (nur in geschlossenen Netzwerken!)\napp.set('trust proxy', 1);\n\n\/\/ Optional: Eigene IP-Extraktion f\u00fcr mehrschichtige Proxys\nconst globalLimiter = rateLimit({\n  windowMs: 15 * 60 * 1000,\n  max: 100,\n  standardHeaders: 'draft-8',\n  legacyHeaders: false,\n  \/\/ Authentifizierte User per ID begrenzen, Fallback auf IP\n  keyGenerator: (req) => {\n    return req.user?.id || req.ip;\n  }\n});<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u00dcberpr\u00fcfe nach der Konfiguration, welche IP dein Server erkennt. Diese Debug-Route nur in der Entwicklungsumgebung aktivieren, niemals in Produktion:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>if (process.env.NODE_ENV !== 'production') {\n  app.get('\/api\/debug-ip', (req, res) => {\n    res.json({\n      ip: req.ip,\n      ips: req.ips,\n      xForwardedFor: req.headers['x-forwarded-for']\n    });\n  });\n}<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"schritt-6-redis-integration-fuer-verteilte-systeme\">Schritt 6: Redis-Integration f\u00fcr verteilte Systeme<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Wenn du mehrere Node.js-Instanzen betreibst (z.B. mit PM2 im Cluster-Modus oder in Kubernetes), speichert jede Instanz ihren eigenen Z\u00e4hler im Arbeitsspeicher. Ein Client, der Anfragen \u00fcber 4 Instanzen verteilt, kann effektiv das 4-fache des Limits senden, ohne gesperrt zu werden. Die L\u00f6sung: ein gemeinsamer Redis-Speicher, auf den alle Instanzen zugreifen.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>npm install @express-rate-limit\/redis ioredis<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Konfiguriere den Redis-Store in <code>index.js<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const rateLimit = require('express-rate-limit');\nconst { RedisStore } = require('@express-rate-limit\/redis');\nconst Redis = require('ioredis');\n\n\/\/ Redis-Verbindung einmalig aufbauen (Singleton-Pattern)\nconst redisClient = new Redis({\n  host: process.env.REDIS_HOST || 'localhost',\n  port: parseInt(process.env.REDIS_PORT) || 6379,\n  password: process.env.REDIS_PASSWORD || undefined,\n  enableReadyCheck: true,\n  maxRetriesPerRequest: 3\n});\n\nredisClient.on('error', (err) => {\n  console.error('Redis-Verbindungsfehler:', err.message);\n  \/\/ Monitoring\/Alerting hier einbinden\n});\n\nredisClient.on('connect', () => {\n  console.log('Redis verbunden');\n});\n\n\/\/ Rate Limiter mit Redis-Backend\nconst distributedLimiter = rateLimit({\n  windowMs: 15 * 60 * 1000,\n  max: 100,\n  standardHeaders: 'draft-8',\n  legacyHeaders: false,\n  store: new RedisStore({\n    sendCommand: (...args) => redisClient.call(...args),\n    prefix: 'rl:'  \/\/ Schl\u00fcssel-Pr\u00e4fix verhindert Kollisionen mit anderen Redis-Keys\n  }),\n  keyGenerator: (req) => req.user?.id || req.ip\n});\n\napp.use(distributedLimiter);<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u00dcberpr\u00fcfe, ob Redis die Z\u00e4hler korrekt speichert. Dazu in einem neuen Terminal:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Terminal 1: Redis-Befehle in Echtzeit beobachten\nredis-cli monitor\n\n# Terminal 2: Anfrage senden\ncurl http:\/\/localhost:3000\/api\/produkte\n\n# Redis-Monitor zeigt jetzt z.B.:\n# 1717600000.123 [0 127.0.0.1:52910] \"SET\" \"rl:127.0.0.1\" \"1\" \"PX\" \"900000\" \"NX\"\n# Bei weiteren Anfragen:\n# \"INCR\" \"rl:127.0.0.1\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Die Redis-Keys laufen automatisch nach Ablauf des Zeitfensters ab (<code>PX<\/code> setzt die TTL in Millisekunden). Du musst dich nicht um das Aufr\u00e4umen alter Z\u00e4hler k\u00fcmmern.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"schritt-7-rate-limiter-flexible-fuer-erweiterte-algorithmen\">Schritt 7: rate-limiter-flexible f\u00fcr erweiterte Algorithmen<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">F\u00fcr komplexere Anforderungen, etwa Token-Bucket-Algorithmus, progressive Blockierung oder Kombinationen aus User-ID und IP, empfiehlt sich <strong>rate-limiter-flexible 11.2.0<\/strong>. Das Paket unterst\u00fctzt Redis, MongoDB, Postgres, MySQL und In-Memory-Speicher mit identischer API und ist eine der flexibelsten Rate-Limiting-L\u00f6sungen f\u00fcr Node.js.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>npm install rate-limiter-flexible<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Implementiere einen kombinierten Limiter, der sowohl IP als auch E-Mail-Adresse ber\u00fccksichtigt. Das verhindert verteilte Angriffe, bei denen ein Angreifer seine Anfragen \u00fcber viele verschiedene IP-Adressen streut:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const { RateLimiterRedis } = require('rate-limiter-flexible');\nconst Redis = require('ioredis');\n\nconst redisClient = new Redis({ host: 'localhost', port: 6379 });\n\n\/\/ Kombinierter Limiter: IP + E-Mail als Schl\u00fcssel\nconst loginLimiter = new RateLimiterRedis({\n  storeClient: redisClient,\n  keyPrefix: 'login_limit',\n  points: 10,          \/\/ 10 Versuche erlaubt\n  duration: 900,       \/\/ pro 15 Minuten (in Sekunden)\n  blockDuration: 900   \/\/ 15 Min. sperren nach \u00dcberschreitung\n});\n\n\/\/ Separater Limiter nur f\u00fcr IP (verhindert IP-\u00fcbergreifende Angriffe auf ein Konto)\nconst loginLimiterByEmail = new RateLimiterRedis({\n  storeClient: redisClient,\n  keyPrefix: 'login_email',\n  points: 5,\n  duration: 900,\n  blockDuration: 900\n});\n\napp.post('\/api\/login-sicher', async (req, res) => {\n  const { email, passwort } = req.body;\n\n  try {\n    \/\/ Beide Limits pr\u00fcfen: IP-basiert und E-Mail-basiert\n    const [ipLimit, emailLimit] = await Promise.all([\n      loginLimiter.consume(req.ip),\n      loginLimiterByEmail.consume(email.toLowerCase())\n    ]);\n\n    \/\/ Anmeldelogik\n    if (email === 'nutzer@beispiel.at' && passwort === 'geheim123') {\n      \/\/ Nach erfolgreichem Login Z\u00e4hler zur\u00fccksetzen\n      await Promise.all([\n        loginLimiter.delete(req.ip),\n        loginLimiterByEmail.delete(email.toLowerCase())\n      ]);\n      res.json({ token: 'jwt-token-platzhalter' });\n    } else {\n      res.status(401).json({\n        fehler: 'Ung\u00fcltige Zugangsdaten',\n        versuche_ip: ipLimit.remainingPoints,\n        versuche_email: emailLimit.remainingPoints\n      });\n    }\n  } catch (err) {\n    \/\/ Rate Limit \u00fcberschritten\n    const retryAfter = Math.round(err.msBeforeNext \/ 1000) || 900;\n    res.set('Retry-After', retryAfter);\n    res.status(429).json({\n      fehler: 'Zu viele Anmeldeversuche.',\n      retryAfterSekunden: retryAfter\n    });\n  }\n});<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"schritt-8-dynamisches-rate-limiting-nach-user-rolle\">Schritt 8: Dynamisches Rate Limiting nach User-Rolle<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Enterprise-APIs unterscheiden oft zwischen Plan-Stufen: Free-Tier-Nutzer erhalten 100 Anfragen pro Stunde, Premium-Nutzer 1.000, interne Services sind unbegrenzt. Statt mehrere Limiter-Instanzen manuell zu verwalten, ist eine dynamische Middleware sauberer und leichter wartbar.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">F\u00fcr diesen Schritt musst du sicherstellen, dass eine JWT-Middleware die Nutzerinformationen in <code>req.user<\/code> schreibt, bevor der dynamische Limiter greift. Wie du eine <a href=\"\/at\/digitale-signatur-nodejs\/\">digitale Signatur und JWT in Node.js implementierst<\/a>, erkl\u00e4rt unser separates Tutorial.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ Limiter nach Plan-Stufe (einmalig anlegen)\nconst limiterNachPlan = {\n  free: rateLimit({\n    windowMs: 60 * 60 * 1000,\n    max: 100,\n    standardHeaders: 'draft-8',\n    legacyHeaders: false\n  }),\n  premium: rateLimit({\n    windowMs: 60 * 60 * 1000,\n    max: 1000,\n    standardHeaders: 'draft-8',\n    legacyHeaders: false\n  }),\n  enterprise: rateLimit({\n    windowMs: 60 * 60 * 1000,\n    max: 10000,\n    standardHeaders: 'draft-8',\n    legacyHeaders: false\n  })\n};\n\n\/\/ Dynamische Middleware: Limiter anhand des User-Plans w\u00e4hlen\nconst dynamischerLimiter = (req, res, next) => {\n  const plan = req.user?.plan || 'free';\n  const limiter = limiterNachPlan[plan] || limiterNachPlan.free;\n  return limiter(req, res, next);\n};\n\n\/\/ Simulierter JWT-Middleware-Stub (in Produktion durch echte JWT-Verifikation ersetzen)\nconst verifiziereJWT = (req, res, next) => {\n  const token = req.headers.authorization?.replace('Bearer ', '');\n  if (token === 'premium-token') {\n    req.user = { id: 'user123', plan: 'premium' };\n  } else if (token === 'enterprise-token') {\n    req.user = { id: 'enterprise1', plan: 'enterprise' };\n  }\n  next();\n};\n\n\/\/ Middleware-Kette: erst JWT pr\u00fcfen, dann dynamisch begrenzen\napp.use('\/api\/v1', verifiziereJWT, dynamischerLimiter);<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"schritt-9-http-429-antworten-professionell-gestalten\">Schritt 9: HTTP 429-Antworten professionell gestalten<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Ein professioneller Rate-Limit-Response enth\u00e4lt mehrere Pflicht-Header, damit Clients wissen, wann sie es erneut versuchen k\u00f6nnen. Das ist besonders wichtig f\u00fcr mobile Apps und API-Clients, die automatisches Retry implementieren. Ein schlechter 429-Response f\u00fchrt zu unkontrolliertem Retry-Flooding, das das Problem verschlimmert statt l\u00f6st.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const professionellerLimiter = rateLimit({\n  windowMs: 15 * 60 * 1000,\n  max: 100,\n  standardHeaders: 'draft-8',\n  legacyHeaders: false,\n  handler: (req, res, next, options) => {\n    const resetZeit = new Date(Date.now() + options.windowMs);\n\n    \/\/ Strukturierter Fehler-Response\n    res.status(options.statusCode).json({\n      fehler: {\n        code: 'RATE_LIMIT_EXCEEDED',\n        nachricht: 'Anfrage-Limit \u00fcberschritten. Bitte warte und versuche es erneut.',\n        limit: options.max,\n        zeitfensterMinuten: options.windowMs \/ 60000,\n        resetZeit: resetZeit.toISOString(),\n        dokumentation: 'https:\/\/api.beispiel.at\/docs\/rate-limits'\n      }\n    });\n  }\n});\n\n\/\/ Globaler Fehlerhandler f\u00fcr konsistente 429-Behandlung\napp.use((err, req, res, next) => {\n  if (err.status === 429) {\n    res.status(429).json({\n      fehler: 'Zu viele Anfragen.',\n      retryAfter: res.getHeader('Retry-After')\n    });\n  } else {\n    next(err);\n  }\n});<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Teste die HTTP-Header der 429-Antwort mit <code>curl -v<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># 11 Login-Versuche senden (Limit ist 10):\nfor i in $(seq 1 11); do\n  curl -s -X POST http:\/\/localhost:3000\/api\/login \\\n    -H \"Content-Type: application\/json\" \\\n    -d '{\"email\":\"test@test.at\",\"passwort\":\"falsch\"}'\n  echo \"\"\ndone\n\n# Beim 11. Versuch erh\u00e4ltst du:\n# {\n#   \"fehler\": {\n#     \"code\": \"RATE_LIMIT_EXCEEDED\",\n#     \"nachricht\": \"Anfrage-Limit \u00fcberschritten...\",\n#     \"limit\": 10,\n#     \"zeitfensterMinuten\": 15,\n#     \"resetZeit\": \"2026-06-17T12:30:00.000Z\"\n#   }\n# }\n\n# HTTP-Header anzeigen:\ncurl -v -X POST http:\/\/localhost:3000\/api\/login \\\n  -H \"Content-Type: application\/json\" \\\n  -d '{\"email\":\"test@test.at\",\"passwort\":\"falsch\"}' 2>&1 | grep -E \"^[<>]\"\n\n# Erwartete Antwort-Header:\n# < HTTP\/1.1 429 Too Many Requests\n# < RateLimit-Limit: 10\n# < RateLimit-Remaining: 0\n# < RateLimit-Reset: 1750163400\n# < Retry-After: 847<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"schritt-10-logging-und-monitoring-fuer-rate-limit-events\">Schritt 10: Logging und Monitoring f\u00fcr Rate-Limit-Events<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Rate-Limit-\u00dcberschreitungen sind wertvolle Sicherheitssignale. Sie k\u00f6nnen auf laufende Angriffe, fehlerhafte API-Clients oder echte Nutzer hinweisen, die ein zu enges Limit erreichen. Ohne systematisches Logging ist es unm\u00f6glich, zwischen einem Angreifer und einem legitimen Nutzer mit hohem Bedarf zu unterscheiden.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const \u00fcberwachterLimiter = rateLimit({\n  windowMs: 15 * 60 * 1000,\n  max: 100,\n  standardHeaders: 'draft-8',\n  legacyHeaders: false,\n  \/\/ Health-Checks vom Limit ausnehmen\n  skip: (req) => req.path === '\/health' || req.path === '\/metrics',\n  handler: (req, res, next, options) => {\n    \/\/ Sicherheits-Event strukturiert loggen\n    const logEintrag = {\n      zeitstempel: new Date().toISOString(),\n      ereignis: 'RATE_LIMIT_UEBERSCHRITTEN',\n      ip: req.ip,\n      pfad: req.path,\n      methode: req.method,\n      userAgent: req.headers['user-agent']?.substring(0, 100),\n      userId: req.user?.id || 'anonym',\n      schweregrad: 'WARNUNG'\n    };\n    console.warn(JSON.stringify(logEintrag));\n\n    \/\/ Hier kannst du z.B. Prometheus-Counter inkrementieren:\n    \/\/ metrics.rateLimitHits.inc({ path: req.path, ip: req.ip });\n\n    res.status(429).json({\n      fehler: 'Zu viele Anfragen.',\n      retryAfterSekunden: Math.ceil(options.windowMs \/ 1000)\n    });\n  }\n});\n\n\/\/ Response-Monitoring-Middleware\napp.use((req, res, next) => {\n  res.on('finish', () => {\n    if (res.statusCode === 429) {\n      console.log(`[RATE-LIMIT] ${new Date().toISOString()} - ${req.ip} - ${req.method} ${req.path}`);\n    }\n  });\n  next();\n});<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"schritt-11-rate-limiting-automatisiert-testen\">Schritt 11: Rate Limiting automatisiert testen<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Manuelles Testen mit Bash-Schleifen ist f\u00fcr CI\/CD nicht reproduzierbar. Schreibe automatisierte Tests mit Jest und supertest, die bei jedem Deploy sicherstellen, dass das Rate Limiting wie erwartet funktioniert:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>npm install --save-dev jest supertest<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ tests\/rateLimit.test.js\nconst request = require('supertest');\n\n\/\/ Frische App-Instanz pro Testdatei (isoliert den Store)\nlet app;\n\nbeforeEach(() => {\n  \/\/ Modul-Cache leeren, damit jeder Test eine neue Instanz bekommt\n  jest.resetModules();\n  app = require('..\/index');\n});\n\ndescribe('Globales Rate Limiting', () => {\n\n  test('erlaubt die ersten 100 Anfragen', async () => {\n    for (let i = 0; i < 100; i++) {\n      const res = await request(app).get('\/api\/produkte');\n      expect(res.status).toBe(200);\n    }\n  });\n\n  test('blockiert die 101. Anfrage mit HTTP 429', async () => {\n    for (let i = 0; i < 100; i++) {\n      await request(app).get('\/api\/produkte');\n    }\n    const res = await request(app).get('\/api\/produkte');\n    expect(res.status).toBe(429);\n    expect(res.body.fehler).toBeDefined();\n  });\n\n  test('liefert korrekte RateLimit-Header', async () => {\n    const res = await request(app).get('\/api\/produkte');\n    expect(res.headers['ratelimit-limit']).toBeDefined();\n    expect(res.headers['ratelimit-remaining']).toBeDefined();\n  });\n\n});\n\ndescribe('Auth-spezifisches Rate Limiting', () => {\n\n  test('blockiert Login nach 10 fehlgeschlagenen Versuchen', async () => {\n    for (let i = 0; i < 10; i++) {\n      await request(app)\n        .post('\/api\/login')\n        .send({ email: 'x@y.at', passwort: 'falsch' });\n    }\n    const res = await request(app)\n      .post('\/api\/login')\n      .send({ email: 'x@y.at', passwort: 'falsch' });\n    expect(res.status).toBe(429);\n    expect(res.body.fehler).toMatch(\/Zu viele\/i);\n  });\n\n});<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">F\u00fcge in <code>package.json<\/code> das Test-Skript hinzu:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\"scripts\": {\n  \"test\": \"jest --testTimeout=30000 --runInBand\"\n}\n\nnpm test\n\n# Erwartete Ausgabe:\n# PASS  tests\/rateLimit.test.js\n#   Globales Rate Limiting\n#     \u2713 erlaubt die ersten 100 Anfragen (2341 ms)\n#     \u2713 blockiert die 101. Anfrage mit HTTP 429 (189 ms)\n#     \u2713 liefert korrekte RateLimit-Header (45 ms)\n#   Auth-spezifisches Rate Limiting\n#     \u2713 blockiert Login nach 10 fehlgeschlagenen Versuchen (312 ms)<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Wichtig:<\/strong> Das Flag <code>--runInBand<\/code> stellt sicher, dass Tests sequenziell laufen und sich die In-Memory-Z\u00e4hler nicht gegenseitig beeinflussen. F\u00fcr parallele Tests m\u00fcsstest du separate Express-Instanzen auf verschiedenen Ports oder echte Redis-Isolation einsetzen.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"schritt-12-nginx-als-vorgelagerten-rate-limiter-nutzen\">Schritt 12: nginx als vorgelagerten Rate Limiter nutzen<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">F\u00fcr Produktionssysteme empfiehlt sich eine zweistufige Architektur: nginx \u00fcbernimmt das grobe Filtern auf Netzwerkebene, Node.js das feink\u00f6rnige, anwendungsbewusste Limiting. Diese Kombination hat zwei Vorteile: Erstens erreichen die teuersten Angriffspakete den Node.js-Prozess gar nicht erst. Zweitens kann nginx wesentlich mehr Verbindungen pro Sekunde verarbeiten als ein Node.js-Prozess, da es kein JavaScript parsen muss.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Das erg\u00e4nzt deine bestehende <a href=\"\/at\/ssh-key-einrichten\/\">SSH-H\u00e4rtung auf Serverebene<\/a> und zusammen mit <a href=\"\/at\/fail2ban-einrichten\/\">Fail2ban<\/a> ergibt sich ein dreischichtiger Schutz: Firewall, nginx-Rate-Limit und Node.js-Limit.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># \/etc\/nginx\/sites-available\/meine-api\n\n# Rate-Limiting-Zonen definieren (10 MB Speicher = ca. 160.000 IPs)\nlimit_req_zone $binary_remote_addr zone=api_global:10m rate=10r\/s;\nlimit_req_zone $binary_remote_addr zone=login_limit:10m rate=1r\/m;\n\nserver {\n    listen 443 ssl http2;\n    server_name api.beispiel.at;\n\n    # SSL-Konfiguration (Let's Encrypt oder eigenes Zertifikat)\n    ssl_certificate \/etc\/letsencrypt\/live\/api.beispiel.at\/fullchain.pem;\n    ssl_certificate_key \/etc\/letsencrypt\/live\/api.beispiel.at\/privkey.pem;\n\n    # Globales nginx-Limit: 10 Anfragen\/Sek, Burst von 20 erlaubt\n    location \/api\/ {\n        limit_req zone=api_global burst=20 nodelay;\n        limit_req_status 429;\n        proxy_pass http:\/\/127.0.0.1:3000;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n        proxy_set_header Host $host;\n    }\n\n    # Strengeres Limit f\u00fcr Login: 1 Anfrage\/Min, Burst von 5\n    location \/api\/login {\n        limit_req zone=login_limit burst=5 nodelay;\n        limit_req_status 429;\n        proxy_pass http:\/\/127.0.0.1:3000;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n    }\n}<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code># Konfiguration pr\u00fcfen und nginx neu laden\nsudo nginx -t\nsudo systemctl reload nginx\n\n# Test: Anfragen an Login-Endpunkt\nfor i in $(seq 1 8); do\n  STATUS=$(curl -s -o \/dev\/null -w \"%{http_code}\" -X POST \\\n    https:\/\/api.beispiel.at\/api\/login \\\n    -H \"Content-Type: application\/json\" \\\n    -d '{\"email\":\"x@y.at\",\"passwort\":\"test\"}')\n  echo \"Anfrage $i: HTTP $STATUS\"\ndone\n\n# nginx-Rate-Limit-Log pr\u00fcfen:\nsudo tail -f \/var\/log\/nginx\/error.log | grep \"limiting requests\"<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"haeufige-fehler-beim-rate-limiting-6-kritische-pitfalls\">H\u00e4ufige Fehler beim Rate Limiting (6 kritische Pitfalls)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Diese Fehler tauchen in Code-Reviews immer wieder auf und hebeln den Schutz vollst\u00e4ndig oder teilweise aus. Alle sechs sind in produktiven Systemen dokumentiert worden.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"pitfall-1-in-memory-store-in-multi-prozess-umgebung\">Pitfall 1: In-Memory-Store in Multi-Prozess-Umgebung<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Problem:<\/strong> Node.js-App l\u00e4uft mit PM2 im Cluster-Modus mit 4 Prozessen. Jeder Prozess hat seinen eigenen In-Memory-Z\u00e4hler. Ein Angreifer, der Anfragen \u00fcber alle 4 Prozesse verteilt, erreicht effektiv das 4-fache Limit ohne gesperrt zu werden.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>L\u00f6sung:<\/strong> Redis-Store verwenden (Schritt 6). F\u00fcr kleinere Projekte ohne Redis: PM2 mit <code>instances: 1<\/code> betreiben oder <code>RateLimiterCluster<\/code> aus rate-limiter-flexible einsetzen, der IPC f\u00fcr die Synchronisation nutzt.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ FALSCH in PM2-Cluster-Modus (jede Instanz z\u00e4hlt separat):\nconst store = new MemoryStore();\n\n\/\/ RICHTIG: Redis als gemeinsamer Speicher f\u00fcr alle Instanzen:\nconst store = new RedisStore({ sendCommand: (...args) => redisClient.call(...args) });<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"pitfall-2-trust-proxy-nicht-konfiguriert\">Pitfall 2: trust proxy nicht konfiguriert<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Problem:<\/strong> Ohne <code>app.set('trust proxy', 1)<\/code> sieht <code>req.ip<\/code> hinter nginx immer <code>127.0.0.1<\/code>. Das Rate Limit gilt f\u00fcr alle Nutzer zusammen, ein einzelner Angreifer kann den Server f\u00fcr alle sperren.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>L\u00f6sung:<\/strong> <code>trust proxy<\/code> korrekt setzen. Den Wert <code>true<\/code> nur in v\u00f6llig kontrollierten internen Netzwerken verwenden. In Produktion lieber den exakten Hop-Count angeben.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"pitfall-3-passwort-reset-und-andere-sensible-endpunkte-vergessen\">Pitfall 3: Passwort-Reset und andere sensible Endpunkte vergessen<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Problem:<\/strong> Entwickler begrenzen den Login-Endpunkt, vergessen aber <code>\/api\/passwort-reset<\/code>, <code>\/api\/email-verifizierung-senden<\/code>, <code>\/api\/otp-code-senden<\/code> und \u00e4hnliche Endpunkte. Diese k\u00f6nnen f\u00fcr E-Mail-Flooding, Telefon-Missbrauch oder Kontoenumerierung eingesetzt werden.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>L\u00f6sung:<\/strong> Alle Endpunkte, die externe Aktionen ausl\u00f6sen (E-Mails, SMS, Webhooks), mit einem strikten Limiter absichern. Als Faustregel: Jeder Endpunkt, der Geld, Zeit oder Aufmerksamkeit des Nutzers kostet, braucht ein Limit.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"pitfall-4-ipv6-subnetze-ignoriert\">Pitfall 4: IPv6-Subnetze ignoriert<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Problem:<\/strong> IPv6 erlaubt einem Angreifer mit einem \/48-Block (65.536 \/64-Netze, je 18 Trilliarden Adressen) praktisch unbegrenzte IP-Rotation. IP-basiertes Rate Limiting wird damit trivial zu umgehen.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>L\u00f6sung:<\/strong> IPv6-Anfragen auf \/64-Subnet-Ebene limitieren, nicht auf einzelne \/128-Adressen:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>keyGenerator: (req) => {\n  const ip = req.ip || '';\n  \/\/ Bei IPv6: ersten 4 Bl\u00f6cke als Subnet-Key verwenden (\/64)\n  if (ip.includes(':')) {\n    return ip.split(':').slice(0, 4).join(':') + '::\/64';\n  }\n  return ip; \/\/ IPv4 unver\u00e4ndert\n}<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"pitfall-5-mobile-nutzer-hinter-carrier-nat-gesperrt\">Pitfall 5: Mobile-Nutzer hinter Carrier-NAT gesperrt<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Problem:<\/strong> Viele Mobilfunk-Nutzer teilen in \u00d6sterreich (A1, T-Mobile, Magenta) dieselbe \u00f6ffentliche IPv4-Adresse \u00fcber Carrier-Grade NAT (CGN). Ein zu enges IP-basiertes Limit sperrt bei \u00dcberschreitung Hunderte legitime Nutzer gleichzeitig.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>L\u00f6sung:<\/strong> F\u00fcr authentifizierte Endpunkte die User-ID als prim\u00e4ren Schl\u00fcssel verwenden, IP nur als Fallback f\u00fcr nicht-authentifizierte Endpunkte. Das Limit f\u00fcr den \u00f6ffentlichen Bereich gro\u00dfz\u00fcgiger setzen (200 statt 100) und auf die Auth-Ebene verlagern.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"pitfall-6-kein-retry-after-header-oder-falsch-befuellter-header\">Pitfall 6: Kein Retry-After-Header oder falsch bef\u00fcllter Header<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Problem:<\/strong> Der API-Client erh\u00e4lt eine 429-Antwort ohne <code>Retry-After<\/code>-Header oder mit einer falschen Zeitangabe. Viele API-Client-Bibliotheken implementieren exponentielles Backoff basierend auf diesem Header. Fehlt er, versuchen Clients oft sofort erneut, was das Problem verschlimmert.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>L\u00f6sung:<\/strong> <code>standardHeaders: 'draft-8'<\/code> setzt alle n\u00f6tigen Header automatisch. Im eigenen Handler immer <code>res.set('Retry-After', retryAfterSekunden)<\/code> setzen, bevor <code>res.status(429).json()<\/code> aufgerufen wird.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"troubleshooting-9-haeufige-probleme-und-ihre-loesungen\">Troubleshooting: 9 h\u00e4ufige Probleme und ihre L\u00f6sungen<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Problem<\/th><th>Wahrscheinliche Ursache<\/th><th>L\u00f6sung<\/th><\/tr><\/thead><tbody><tr><td><code>req.ip<\/code> zeigt immer <code>127.0.0.1<\/code><\/td><td><code>trust proxy<\/code> nicht gesetzt<\/td><td><code>app.set('trust proxy', 1)<\/code> vor allen Middleware-Aufrufen<\/td><\/tr><tr><td>429 nach 1 Anfrage (nicht nach dem konfigurierten Limit)<\/td><td>Redis-Verbindung fehlgeschlagen; Fallback-Z\u00e4hler auf 0 gestartet<\/td><td>Redis-Status mit <code>redis-cli ping<\/code> pr\u00fcfen; Verbindungs-Fehlerbehandlung hinzuf\u00fcgen<\/td><\/tr><tr><td>RateLimit-Header fehlen in der Antwort<\/td><td><code>standardHeaders<\/code> nicht gesetzt oder auf Wert \"false\"<\/td><td><code>standardHeaders: 'draft-8'<\/code>, <code>legacyHeaders: false<\/code> setzen<\/td><\/tr><tr><td>Rate Limit gilt nicht f\u00fcr alle PM2-Instanzen<\/td><td>In-Memory-Store bei PM2-Cluster: jede Instanz z\u00e4hlt separat<\/td><td>Redis-Store oder <code>RateLimiterCluster<\/code> einsetzen<\/td><\/tr><tr><td>Jest-Tests scheitern zuf\u00e4llig<\/td><td>Z\u00e4hler aus vorherigem Test l\u00e4uft noch; Tests laufen parallel<\/td><td><code>--runInBand<\/code> Flag und <code>jest.resetModules()<\/code> in <code>beforeEach<\/code><\/td><\/tr><tr><td>Eigene Nutzer werden gesperrt<\/td><td>Limit zu eng oder Mobile-NAT-IP<\/td><td>User-ID als Schl\u00fcssel statt IP; Limit f\u00fcr unauthentifizierte Endpunkte erh\u00f6hen<\/td><\/tr><tr><td>Angreifer umgeht Limit durch IPv6-Rotation<\/td><td>Limit auf \/128-Einzeladressen statt \/64-Subnetz<\/td><td>IPv6-Subnet-Key-Generator implementieren (Pitfall 4)<\/td><\/tr><tr><td><code>Too many connections<\/code> in Redis<\/td><td>Jeder Request \u00f6ffnet eine neue Redis-Verbindung<\/td><td>Redis-Client als Singleton anlegen und per Dependency Injection \u00fcbergeben<\/td><\/tr><tr><td>Rate-Limit-Z\u00e4hler nach Deploy zur\u00fcckgesetzt<\/td><td>In-Memory-Store wird beim Neustart geleert<\/td><td>Redis-Store f\u00fcr persistente Z\u00e4hler \u00fcber Deployments hinweg verwenden<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"fortgeschrittene-techniken-tarpitting-und-progressive-blocking\">Fortgeschrittene Techniken: Tarpitting und Progressive Blocking<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Standard-Rate-Limiting blockiert sofort mit HTTP 429. Fortgeschrittene Systeme nutzen zwei weitere Techniken, die Angreifer erheblich effektiver abschrecken, weil sie den Zeitaufwand f\u00fcr automatisierte Angriffe dramatisch erh\u00f6hen.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Tarpitting (K\u00fcnstliche Verz\u00f6gerung):<\/strong> Statt sofort 429 zu senden, wartet der Server mehrere Sekunden, bevor er antwortet. Das verlangsamt Angreifer, die automatisierte Tools einsetzen, erheblich, ohne legitime Nutzer zu beeintr\u00e4chtigen, da diese das Limit nicht erreichen:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ Verd\u00e4chtige Anfragen k\u00fcnstlich verz\u00f6gern\nconst tarpit = async (req, res, next) => {\n  const verdaechtig = !req.headers['authorization'] &&\n                      req.headers['user-agent']?.includes('python-requests');\n  if (verdaechtig) {\n    \/\/ 3 Sekunden warten, bevor weitergemacht wird\n    await new Promise(resolve => setTimeout(resolve, 3000));\n  }\n  next();\n};\n\napp.post('\/api\/login', tarpit, authLimiter, loginHandler);<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Progressive Blocking (Eskalierendes Sperren):<\/strong> Nach der ersten \u00dcberschreitung wird 1 Minute gesperrt, nach der zweiten 10 Minuten, nach der dritten 1 Stunde. Das macht automatisierte Angriffe exponentiell teurer:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const { RateLimiterMemory } = require('rate-limiter-flexible');\n\n\/\/ Drei Eskalationsstufen\nconst limiterStufe1 = new RateLimiterMemory({ points: 10, duration: 60 });\nconst limiterStufe2 = new RateLimiterMemory({ points: 3, duration: 600, blockDuration: 600 });\nconst limiterStufe3 = new RateLimiterMemory({ points: 1, duration: 3600, blockDuration: 3600 });\n\nconst progressiveProtection = async (req, res, next) => {\n  try {\n    await limiterStufe1.consume(req.ip);\n    next();\n  } catch (e1) {\n    try {\n      await limiterStufe2.consume(req.ip);\n      console.warn(`Warnung: ${req.ip} auf Eskalationsstufe 2`);\n      next();\n    } catch (e2) {\n      try {\n        await limiterStufe3.consume(req.ip);\n        console.error(`Kritisch: ${req.ip} auf Eskalationsstufe 3`);\n        next();\n      } catch (e3) {\n        res.status(429).json({\n          fehler: 'IP tempor\u00e4r gesperrt.',\n          dauerStunden: 1\n        });\n      }\n    }\n  }\n};<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Diese Techniken erg\u00e4nzen sich gut mit netzwerkseitiger Absicherung. Kombiniert mit <a href=\"\/at\/wireguard-einrichten\/\">WireGuard VPN f\u00fcr interne Services<\/a> und <a href=\"\/at\/ssh-key-einrichten\/\">SSH-Key-Authentifizierung<\/a> entsteht eine mehrschichtige Verteidigung.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"vollstaendiges-beispielprojekt\">Vollst\u00e4ndiges Beispielprojekt<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Die folgende <code>index.js<\/code> fasst alle Schritte dieses Tutorials zu einer produktionsreifen Datei zusammen. Sie enth\u00e4lt globales und routen-spezifisches Rate Limiting, korrektes Proxy-Handling, strukturiertes Logging und einen testbaren App-Export.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>'use strict';\nrequire('dotenv').config(); \/\/ npm install dotenv\nconst express = require('express');\nconst rateLimit = require('express-rate-limit');\n\nconst app = express();\nconst PORT = process.env.PORT || 3000;\nconst NODE_ENV = process.env.NODE_ENV || 'development';\n\n\/\/ Proxy-Konfiguration (anpassen je nach Infrastruktur)\napp.set('trust proxy', 1);\napp.use(express.json());\n\n\/\/ \u2500\u2500 Logging-Helper \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst logRateLimit = (req, level = 'WARNUNG') => {\n  console.warn(JSON.stringify({\n    zeitstempel: new Date().toISOString(),\n    ereignis: 'RATE_LIMIT',\n    schweregrad: level,\n    ip: req.ip,\n    pfad: req.path,\n    methode: req.method,\n    userId: req.user?.id || 'anonym'\n  }));\n};\n\n\/\/ \u2500\u2500 Limiter-Definitionen \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst globalLimiter = rateLimit({\n  windowMs: 15 * 60 * 1000,\n  max: 100,\n  standardHeaders: 'draft-8',\n  legacyHeaders: false,\n  keyGenerator: (req) => req.user?.id || req.ip,\n  skip: (req) => ['\/health', '\/metrics'].includes(req.path),\n  handler: (req, res, next, options) => {\n    logRateLimit(req, 'WARNUNG');\n    res.status(429).json({\n      fehler: 'Zu viele Anfragen. Bitte warte 15 Minuten.',\n      retryAfterSekunden: Math.ceil(options.windowMs \/ 1000)\n    });\n  }\n});\n\nconst authLimiter = rateLimit({\n  windowMs: 15 * 60 * 1000,\n  max: 10,\n  standardHeaders: 'draft-8',\n  legacyHeaders: false,\n  skipSuccessfulRequests: true,\n  handler: (req, res, next, options) => {\n    logRateLimit(req, 'KRITISCH');\n    res.status(429).json({\n      fehler: 'Zu viele Anmeldeversuche. Warte 15 Minuten.',\n      code: 'AUTH_RATE_LIMIT'\n    });\n  }\n});\n\n\/\/ \u2500\u2500 Globales Limit f\u00fcr alle Routen \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\napp.use(globalLimiter);\n\n\/\/ \u2500\u2500 \u00d6ffentliche Routen \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\napp.get('\/health', (req, res) => res.json({ status: 'ok', umgebung: NODE_ENV }));\n\napp.get('\/api\/produkte', (req, res) => {\n  res.json({ produkte: ['Laptop', 'Maus', 'Tastatur'] });\n});\n\n\/\/ \u2500\u2500 Auth-Routen (striktes Limit) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\napp.post('\/api\/login', authLimiter, (req, res) => {\n  const { email, passwort } = req.body || {};\n  if (!email || !passwort) {\n    return res.status(400).json({ fehler: 'E-Mail und Passwort erforderlich' });\n  }\n  if (email === 'nutzer@beispiel.at' && passwort === 'geheim123') {\n    return res.json({ token: 'jwt-token-platzhalter' });\n  }\n  res.status(401).json({ fehler: 'Ung\u00fcltige Zugangsdaten' });\n});\n\napp.post('\/api\/passwort-reset', authLimiter, (req, res) => {\n  const { email } = req.body || {};\n  if (!email) return res.status(400).json({ fehler: 'E-Mail erforderlich' });\n  res.json({ nachricht: 'Falls das Konto existiert, wurde ein Reset-Link gesendet.' });\n});\n\n\/\/ \u2500\u2500 Server starten \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nif (require.main === module) {\n  app.listen(PORT, () => {\n    console.log(`Server l\u00e4uft auf Port ${PORT} [${NODE_ENV}]`);\n  });\n}\n\nmodule.exports = app;<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"weiterfuehrende-artikel\">Weiterf\u00fchrende Artikel<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"verwandte-themen-auf-shattered-io\">Verwandte Themen auf shattered.io<\/h3>\n\n\n\n<ul class=\"wp-block-list\"><li><a href=\"\/at\/digitale-signatur-nodejs\/\">Digitale Signatur in Node.js: 11 Schritte, 40 Minuten<\/a><\/li><li><a href=\"\/at\/ssh-key-einrichten\/\">SSH-Key einrichten: Server h\u00e4rten in 10 Schritten<\/a><\/li><li><a href=\"\/at\/fail2ban-einrichten\/\">Fail2ban einrichten: SSH-Schutz in 12 Schritten<\/a><\/li><li><a href=\"\/at\/cyberangriffe-dach-2026\/\">Cyberangriffe DACH 2026: 289 Mrd. Euro Schaden<\/a><\/li><li><a href=\"\/at\/wireguard-einrichten\/\">WireGuard einrichten: VPN-Server in 12 Schritten<\/a><\/li><\/ul>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"faq-rate-limiting-in-node-js\">FAQ: Rate Limiting in Node.js<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"was-ist-der-unterschied-zwischen-rate-limiting-und-throttling\">Was ist der Unterschied zwischen Rate Limiting und Throttling?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Rate Limiting blockiert Anfragen hart, sobald das Limit erreicht ist (HTTP 429). Throttling verz\u00f6gert sie stattdessen, d.h. die Anfrage wird langsamer bearbeitet, aber nicht abgelehnt. F\u00fcr Sicherheitszwecke ist Rate Limiting mit hartem Blocking besser geeignet. Throttling passt eher zu Szenarien, wo ein reibungsloses Nutzererlebnis wichtiger ist als strikte Durchsetzung, etwa bei Streaming-APIs.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"welchen-algorithmus-soll-ich-fuer-den-login-endpunkt-verwenden\">Welchen Algorithmus soll ich f\u00fcr den Login-Endpunkt verwenden?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">F\u00fcr Login-Endpunkte empfiehlt sich der Token-Bucket-Algorithmus \u00fcber <code>rate-limiter-flexible<\/code> kombiniert mit progressivem Blocking: Nach 5 fehlgeschlagenen Versuchen 1 Minute sperren, nach 10 insgesamt 15 Minuten, nach 20 den Account tempor\u00e4r deaktivieren und den Nutzer per E-Mail benachrichtigen. <code>skipSuccessfulRequests: true<\/code> stellt sicher, dass normale Nutzer nicht bestraft werden.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"brauche-ich-redis-fuer-rate-limiting-in-einem-kleinen-projekt\">Brauche ich Redis f\u00fcr Rate Limiting in einem kleinen Projekt?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Nein. Wenn du eine einzelne Node.js-Instanz betreibst (kein PM2-Cluster, kein Kubernetes, kein Load Balancer mit mehreren Instanzen), reicht der In-Memory-Store von express-rate-limit aus. Der einzige Nachteil: Beim Neustart des Servers werden alle Z\u00e4hler zur\u00fcckgesetzt. Angreifer, die einen Neustart erzwingen k\u00f6nnen, umgehen das Limit damit kurz. F\u00fcr produktive Systeme mit Skalierungsanforderungen ist Redis unverzichtbar.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"wie-nehme-ich-vertrauenswuerdige-ips-vom-limit-aus\">Wie nehme ich vertrauensw\u00fcrdige IPs vom Limit aus?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Verwende die <code>skip<\/code>-Option von express-rate-limit, um interne IPs oder Health-Check-Adressen vom Limit auszunehmen. Achtung: Diese IPs sollten aus einer Konfigurationsdatei oder Umgebungsvariable kommen, nie fest im Code stehen:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const VERTRAUENSWUERDIGE_IPS = new Set(\n  (process.env.WHITELISTED_IPS || '').split(',').filter(Boolean)\n);\n\nconst limiter = rateLimit({\n  windowMs: 15 * 60 * 1000,\n  max: 100,\n  skip: (req) => VERTRAUENSWUERDIGE_IPS.has(req.ip)\n});<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"welche-http-header-sendet-express-rate-limit-8-x\">Welche HTTP-Header sendet express-rate-limit 8.x?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Mit <code>standardHeaders: 'draft-8'<\/code> sendet express-rate-limit 8.5.2 die Header <code>RateLimit-Limit<\/code>, <code>RateLimit-Remaining<\/code>, <code>RateLimit-Reset<\/code> und bei \u00dcberschreitung <code>Retry-After<\/code>, gem\u00e4\u00df dem IETF-Entwurf f\u00fcr HTTP Rate Limit Headers. Die alten <code>X-RateLimit-*<\/code>-Header aus Version 6.x sind mit <code>legacyHeaders: false<\/code> deaktiviert.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"ersetzt-rate-limiting-eine-web-application-firewall\">Ersetzt Rate Limiting eine Web Application Firewall?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Nein. Rate Limiting und WAF sind komplement\u00e4re Sicherheitsschichten. Eine WAF erkennt Angriffsmuster im Payload (SQL-Injection, XSS, Path Traversal). Rate Limiting begrenzt die Anzahl der Anfragen, unabh\u00e4ngig vom Inhalt. Beide Schichten geh\u00f6ren in eine produktionsreife Sicherheitsarchitektur. F\u00fcr \u00d6sterreich und die DACH-Region bieten Anbieter wie Cloudflare, AWS WAF und Fastly fertige WAF-L\u00f6sungen.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"kann-ein-angreifer-ip-basiertes-rate-limiting-umgehen\">Kann ein Angreifer IP-basiertes Rate Limiting umgehen?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Ja, durch IP-Rotation \u00fcber Proxys, Tor, gemietete Botnetze oder IPv6-Adress-Rotation. Kombiniere deshalb mehrere Identifizierungssignale: IP-Adresse, User-Agent, Device-Fingerprint, Account-ID und Verhaltensmuster. F\u00fcr hochwertige Endpunkte bietet sich zus\u00e4tzlich ein CAPTCHA nach einer bestimmten Anzahl Fehlversuche an, das automatisierte Rotation unwirksam macht.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"wie-setze-ich-rate-limit-zaehler-im-jest-test-zurueck\">Wie setze ich Rate-Limit-Z\u00e4hler im Jest-Test zur\u00fcck?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Bei In-Memory-Store: <code>jest.resetModules()<\/code> in <code>beforeEach<\/code> aufrufen und die App-Instanz neu laden. Das erstellt einen frischen Z\u00e4hler. Bei Redis-Store: Den betreffenden Key l\u00f6schen mit <code>redis-cli DEL \"rl:127.0.0.1\"<\/code> oder programmatisch mit <code>redisClient.del('rl:127.0.0.1')<\/code>. Alternativ einen Test-spezifischen Key-Prefix verwenden, der nach dem Test vollst\u00e4ndig gel\u00f6scht wird.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Rate Limiting in Node.js sch\u00fctzt deine API vor Brute-Force-Angriffen, DDoS-Wellen und automatisierten Scrapers. Wer keinen Anfrage-Begrenzer einsetzt, riskiert Serverausf\u00e4lle, gestohlene Zugangsdaten und hohe Cloud-Rechnungen. Dieses Tutorial zeigt dir in 12\u2026<\/p>\n","protected":false},"author":4,"featured_media":118,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[3],"tags":[],"class_list":["post-117","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-security"],"_links":{"self":[{"href":"https:\/\/shattered.io\/at\/wp-json\/wp\/v2\/posts\/117","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/shattered.io\/at\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/shattered.io\/at\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/shattered.io\/at\/wp-json\/wp\/v2\/users\/4"}],"replies":[{"embeddable":true,"href":"https:\/\/shattered.io\/at\/wp-json\/wp\/v2\/comments?post=117"}],"version-history":[{"count":0,"href":"https:\/\/shattered.io\/at\/wp-json\/wp\/v2\/posts\/117\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/shattered.io\/at\/wp-json\/wp\/v2\/media\/118"}],"wp:attachment":[{"href":"https:\/\/shattered.io\/at\/wp-json\/wp\/v2\/media?parent=117"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/shattered.io\/at\/wp-json\/wp\/v2\/categories?post=117"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/shattered.io\/at\/wp-json\/wp\/v2\/tags?post=117"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}