Eine Anwendung kann schon bei kleiner Last ausfallen, wenn jede Anfrage eine neue Kombination aus Suchbegriff oder Filtern ist und der Server sie bei jedem Aufruf neu rendert: Dann trifft keine Anfrage einen Cache, jede startet frische Datenbankabfragen, und die wartende Arbeit wächst schneller, als der Verbindungspool der Datenbank sie abbaut. Die Datenbank ist der Ort, an dem die Arbeit landet; ausfallen kann der Server oder die Anwendung davor. In einem von uns dokumentierten Fall, einer von IT4OWL selbst betriebenen Vergleichsplattform, fiel die Anwendung in der stärksten Stunde des erfassten Zeitraums aus; diese Stunde brachte im Schnitt nur etwa 21 Anfragen pro Sekunde (Cloudflare-Schätzung). Link11 beschreibt das Muster als Layer-7-Angriff auf CPU und Datenbanken, etwa durch „manipulierte Suchanfragen auf dynamischen Webseiten"1. Gegen solche Last helfen drei Schritte: erst messen, welche Pfade wirklich teuer sind, dann diese Pfade mit einem gemeinsamen Budget begrenzen, das nicht an einzelne IP-Adressen gebunden ist, und wo möglich vorab oder gecacht statt dynamisch ausliefern.
Zuerst messen: welche Pfade sind wirklich teuer?
Ob ein Endpunkt teuer ist, entscheidet die Messung, nicht die Vermutung; ob WooCommerce-Filter oder AJAX-Aufrufe in Ihrem Shop langsam sind, hängt von Katalog, Plugins und Datenbank ab. Schreiben Sie dafür die Antwortzeit ins nginx-Log:
log_format diag '$time_iso8601 $status $request_time $request_uri';
access_log /var/log/nginx/diag.log diag;
Summieren Sie dann die Zeit je Pfad ohne Query-String und zählen Sie, wie viele verschiedene URLs mit Query-String vorkamen:
awk '{split($4,u,"?"); t[u[1]]+=$3; n[u[1]]++}
END {for (p in t) printf "%8.1f s %7d %s\n", t[p], n[p], p}' diag.log | sort -rn | head
awk '$4 ~ /\?/ {print $4}' diag.log | sort -u | wc -l
Die Zahl verschiedener URLs mit Query-String, verglichen mit der Zahl aller Anfragen, zeigt, wie viel ein Cache überhaupt abfangen könnte. Im von uns dokumentierten Fall kamen auf 100 Filteranfragen rund 64 verschiedene URLs, und am 4. bis 6. Oktober traf 99,7 % der Datensätze (Stichprobe) eine URL, die höchstens zweimal vorkam. Eine einzelne Filteranfrage war dort mit einem Median von 678 ms nur etwa das 1,1- bis 1,3-Fache einer normalen Seite (623 ms) teuer. Die Anwendung brach also nicht an einer einzelnen teuren Anfrage, sondern weil eine hohe Rate neuer Kombinationen an jedem Cache vorbei Datenbank- und Facetten-Arbeit auslöste.
WordPress-Suche und WooCommerce-Filter
Die WordPress-Suche läuft über die URL home_url( '?s=' . urlencode( $search ) )2, das heißt über einen Query-Parameter auf der Startseite. Bei WooCommerce zeigt die Suchleiste Vorschläge per AJAX, „without reloading the page"3, und ein Attributfilter „finds matching products and shows them on the shop page"4. Jede dieser Anfragen kann ungecacht bis in die Datenbank gehen. Welche URLs genau das auslösen, sehen Sie im Netzwerk-Tab Ihres Browsers und in der Auswertung oben.
Für solche Pfade begrenzt ein gemeinsames Budget die Last, unabhängig davon, von wie vielen Adressen sie kommt. Mit limit_req_zone legt nginx einen Schlüssel und eine Rate fest; Anfragen mit leerem Schlüssel werden nicht gezählt5. Die folgende Konfiguration nutzt das: Nur Anfragen mit Suchparameter s bekommen den gemeinsamen Schlüssel search, alle anderen Anfragen bleiben unbegrenzt.
# http-Kontext
map $arg_s $search_key {
default "search";
"" "";
}
limit_req_zone $search_key zone=search_budget:1m rate=5r/s;
limit_req_status 429;
# server-Kontext
error_page 429 /suche-ausgelastet.html;
location = /suche-ausgelastet.html {
internal;
root /var/www/fallback; # Warteseite liegt unter /var/www/fallback/suche-ausgelastet.html
}
# in dem location-Block, der die WordPress-Anfragen verarbeitet
limit_req zone=search_budget burst=10 nodelay;
Ohne limit_req_status 429 antwortet nginx auf abgewiesene Anfragen mit 503, nicht mit 4295. Die Warteseite muss unter dem angegebenen root als Datei existieren und für nginx lesbar sein; sonst bekommen abgewiesene Besucher nicht die Warteseite. Prüfen Sie das nach dem Einrichten mit einer Testanfrage über dem Limit. Wir haben diese Konfiguration in einer Testumgebung mit nginx -t geprüft: Mit vorhandener Datei lieferten abgewiesene Anfragen 429 und den Inhalt der Warteseite, bei direktem Aufruf der Warteseite kam 404, weil sie internal ist.
Die nginx-Direktive map weist im Beispiel jedem Aufruf mit Parameter s (der WordPress-Suche ?s=) den Schlüssel search zu; alle anderen Aufrufe bekommen einen leeren Schlüssel und werden nicht gezählt6. Die 5 Anfragen pro Sekunde sind ein Platzhalter: Setzen Sie die Rate knapp über Ihre normale Spitze, die Sie aus dem Log kennen. Mit burst=10 nodelay werden kurze Spitzen sofort bedient, Überschuss wird abgewiesen5. Für Shop-Filter gilt dasselbe Muster mit map $args $filter_key { default "filter"; "" ""; } auf den Pfaden, auf denen jede Query-Kombination serverseitig gerendert wird; Tracking-Parameter wie utm_source zählen dann mit, also begrenzen Sie nur auf die Filter-Pfade.
Ein globales Budget hat einen bewussten Preis: Während einer Welle bekommen auch echte Besucher bei der Suche eine Warteseite, der übrige Shop soll erreichbar bleiben.
Next.js: Seiten, die bei jedem Aufruf rendern
Beim Pages Router von Next.js läuft getServerSideProps „on every request instead of on build time"7. Eine Kategorie- oder Suchseite mit dieser Funktion rendert jede einzigartige URL neu. Incremental Static Regeneration kann die Serverlast senken, weil die meisten Anfragen eine vorgerenderte statische Seite erhalten8. Ein Beispiel:
// pages/kategorie/[slug].js
export async function getStaticPaths() {
return { paths: bekannteSlugs.map((slug) => ({ params: { slug } })), fallback: false }
}
export async function getStaticProps({ params }) {
const daten = await ladeKategorie(params.slug)
if (!daten) return { notFound: true }
return { props: { daten }, revalidate: 60 } // Sekunden
}
revalidate ist in Sekunden angegeben9. Beim Pages Router ist fallback: false in getStaticPaths die entscheidende Einstellung: Pfade, die nicht in paths stehen, liefern 40410. Mit fallback: 'blocking' würde dagegen jeder neue Pfad einmal erzeugt und gecacht10, sodass unbegrenzt viele neue Pfade unbegrenzt viele Seiten erzeugen könnten. Im App Router von Next.js macht schon das Lesen von searchParams die Seite dynamisch („opt the page into dynamic rendering at request time")11. Wird eine Route vorgerendert, rendert useSearchParams den Client-Komponenten-Baum bis zur nächsten Suspense-Grenze im Browser12. Lesen Sie searchParams deshalb nur dort, wo die Seite ohnehin dynamisch sein muss, und halten Sie die Zahl zulässiger Werte klein.
Wenn die Kombinationen unbegrenzt sind
Bei freien Filterkombinationen reicht ein Cache nicht, weil der Raum der Kombinationen zu groß ist. Dann hilft nur, die Arbeit pro unbekannter Kombination zu deckeln: eine Obergrenze für wartende Abfragen statt einer Warteschlange ohne Limit, eine Obergrenze für gleichzeitige Datenbankabfragen und ein Budget je Sitzung und insgesamt. Im dokumentierten Fall hatte der Datenbankpool 16 Verbindungen mit 20 Sekunden Wartebudget; am 4. Oktober enthält das Anwendungslog 19.089 Zeitüberschreitungen beim Beschaffen einer Verbindung, danach lief der Node.js-Prozess in sein Heap-Limit und beendete sich. Als Mechanismus halten wir für plausibel, aber nicht durch ein Heap-Profil bewiesen: Die Liste wartender Abfragen war je Kombination ohne Obergrenze angelegt und dedupliziert nur identische Abfragen, sodass viele neue Kombinationen mehr Arbeit anstauten, als der Pool abbaute, und diese Arbeit lag im Heap. Am 14. Oktober ging im dokumentierten Fall eine Variante live, in der eigene Filterkombinationen eine signierte, unsichtbare Browsersitzung und Budgets voraussetzen; sie kam nach der Hauptwelle, eine messbare Wirkung lässt sich aus den Daten nicht ablesen.
Im dokumentierten Fall beendete ein Eingriff vor der Anwendung die Ausfälle: Der Webserver erkannte die Anfragen anhand ihrer Form und beantwortete sie mit einer vorgerenderten statischen Seite, ohne Anwendung und Datenbank. In den rund 23 Stunden danach lieferte er 295.587 solcher Antworten, bei einem einzigen Upstream-Fehler. Die Antwortzeit dieser statischen Antworten lag mit einem Median von 265 ms unter der einer von der Anwendung gerenderten Filterseite (678 ms).
Wann das nicht reicht
Ein gemeinsames Budget für teure Pfade, wie es der nginx-Abschnitt oben zeigt, schützt die Datenbank, schließt aber echte Nutzer mit aus, sobald es greift. Es setzt außerdem voraus, dass Sie den teuren Pfad kennen; ein Angriff auf einen Pfad, den Sie nicht gemessen haben, läuft daran vorbei. Und es begrenzt nur, was nginx sieht: Läuft die Last an Cloudflare vorbei direkt auf den Server, hilft zuerst die Sperre des Origin (siehe Cloudflare ist aktiv, trotzdem geht die Seite down). Wenn Anfragen nach Form schwer von echtem Verkehr zu trennen sind, braucht es eine Klassifikation vor der Anwendung, und die muss man auf den Fall zuschneiden.
Quellen
- Link11: DDoS Protection Buyer's Guide (DE), S. 8. https://www.link11.com/wp-content/uploads/2025/11/link11_ddos_protection_buyers_guide_de.pdf, geprüft am 11.11.2026
- WordPress Developer Resources: get_search_link(). https://developer.wordpress.org/reference/functions/get_search_link/, geprüft am 11.11.2026
- WooCommerce: Troubleshoot AJAX issues. https://woocommerce.com/document/troubleshoot-ajax-issues/, geprüft am 11.11.2026
- WooCommerce Product Search: Attribute filter block. https://woocommerce.com/document/woocommerce-product-search/blocks/product-filter-attributes/, geprüft am 11.11.2026
- nginx: ngx_http_limit_req_module. https://nginx.org/en/docs/http/ngx_http_limit_req_module.html, geprüft am 11.11.2026
- nginx: ngx_http_map_module. https://nginx.org/en/docs/http/ngx_http_map_module.html, geprüft am 11.11.2026
- Next.js Docs: Server-side Rendering (Pages Router). https://nextjs.org/docs/pages/building-your-application/rendering/server-side-rendering, geprüft am 11.11.2026
- Next.js Docs: Incremental Static Regeneration. https://nextjs.org/docs/app/guides/incremental-static-regeneration, geprüft am 11.11.2026
- Next.js Docs: getStaticProps, revalidate. https://nextjs.org/docs/15/pages/api-reference/functions/get-static-props, geprüft am 11.11.2026
- Next.js Docs: getStaticPaths. https://nextjs.org/docs/pages/api-reference/functions/get-static-paths, geprüft am 11.11.2026
- Next.js Docs: page.js, searchParams. https://nextjs.org/docs/app/api-reference/file-conventions/page, geprüft am 11.11.2026
- Next.js Docs: useSearchParams. https://nextjs.org/docs/app/api-reference/functions/use-search-params, geprüft am 12.11.2026