Load Testing für Ihr SaaS, bevor es in Produktion bricht

#Load Testing SaaS
Sandor Farkas - Founder & Lead Developer at Wolf-Tech

Sandor Farkas

Gründer & Lead Developer

Experte für Softwareentwicklung und Legacy-Code-Optimierung

Der klassische SaaS-Ausfall passiert an einem Dienstagmorgen. Das Produkt landet in einem populären Newsletter, ein Kunde startet eine Kampagne an seine gesamte Liste, oder das Sales-Team schließt einen Deal ab, der fünfhundert Seats auf einmal onboardet. Requests stauen sich, die Datenbank beginnt auf die Festplatte auszulagern, Worker-Queues laufen voll, und das Dashboard, das letzten Monat 99,9 % Uptime zeigte, zeigt für die nächsten neunzig Minuten einen roten Balken. Niemand hat schlechten Code geschrieben. Die Anwendung ist schlicht auf eine Traffic-Form getroffen, gegen die sie nie gemessen wurde – und genau unter dieser unerwarteten Last bricht sie.

Load Testing für SaaS ist die Disziplin, diese Form absichtlich zu messen, in einer kontrollierten Umgebung, bevor es in Produktion passiert. Es ist keine einmalige Checkbox vor dem Launch. Es ist eine wiederkehrende Praxis, die drei konkrete Fragen beantwortet: Wie viel Traffic kann dieses System heute verkraften, wo bricht es zuerst, und degradiert es kontrolliert oder katastrophal. Teams, die Load Testing als fortlaufende Engineering-Arbeit behandeln – nicht als QA-Ritual – sind diejenigen, die ohne die Dienstagmorgen-Überraschung skalieren.

Dieser Beitrag behandelt, wie man realistische Lasttests entwirft, die zwei Tools, die fast jeden Bedarf abdecken (k6 und Locust), wie man die Ergebnisse interpretiert und die Engpassmuster, die wir in PHP/Symfony- und Next.js-Anwendungen immer wieder sehen.

Warum die meisten Lasttests die echten Grenzen verfehlen

Ein Lasttest ist nur nützlich, wenn er dem Traffic ähnelt, der tatsächlich auf Produktion trifft. Drei Muster lassen die meisten Versuche scheitern.

Das erste ist, einen einzelnen Endpoint in einer engen Schleife zu treffen. Ein Hammering-Skript auf GET /api/products zeigt fröhlich zehntausend Requests pro Sekunde, weil ein einzelner Query-Plan und eine einzelne Response sauber in MySQLs Buffer Pool und PHPs Opcode-Cache passen. Echter Traffic verteilt sich über Dutzende Endpoints mit unterschiedlichen Query-Kosten, Cache-Hit-Raten und nachgelagerten Abhängigkeiten. Der cache-freundliche Endpoint sagt nichts über den authentifizierten Such-Endpoint aus, der sechs Tabellen joint.

Das zweite ist synthetisches Datenmaterial, das nicht der Produktionsform entspricht. Eine Testdatenbank mit tausend Nutzern und zehntausend Bestellungen wird die ORDER BY created_at DESC LIMIT 20-Query nicht offenbaren, die zum Full Table Scan wird, sobald die Bestelltabelle zehn Millionen Zeilen überschreitet. Lasttests müssen gegen einen Datenbestand laufen, dessen Größe und Verteilung der Produktion entspricht oder sie übertrifft.

Das dritte ist das Ignorieren von Think Time und Session-Struktur. Echte Nutzer authentifizieren sich, navigieren, pausieren zum Lesen, senden ein Formular ab und loggen sich aus. Ein Lasttest, der Requests ohne Pausen abfeuert, misst Ihr System unter einer Last, die so nie auftritt. Schlimmer: Er verdeckt oft Engpässe, die erst auftreten, wenn Connection Pools leerlaufen und sich Sessions in Redis ansammeln.

Ein realistischer Lasttest modelliert User Journeys, nicht Endpoints. Er nutzt produktionsrepräsentative Daten. Er enthält den Authentifizierungs-Flow, den langlaufenden Report-Export und den Webhook, der bei jedem Update feuert. Er verteilt virtuelle Nutzer über die Szenarien in ungefähr demselben Verhältnis, das Ihre Analytics für echte Nutzer zeigen.

Tool-Wahl: k6 vs. Locust

Für die meisten Teams fällt die Wahl zwischen k6 und Locust. Beide sind Open Source, beide liefern glaubwürdige Ergebnisse, und beide skalieren auf zehntausende virtuelle Nutzer auf bescheidener Hardware.

k6 ist in Go geschrieben, wird in JavaScript geskriptet und ist um eine Streaming-Metrik-Pipeline herum entworfen. Es hat exzellente CI/CD-Integration, produziert strukturierte Ausgaben, die direkt an Prometheus oder Grafana Cloud gehen, und skaliert effizient, weil jeder virtuelle Nutzer eine Goroutine statt eines Prozesses ist. Wir greifen standardmäßig zu k6 bei Teams, die bereits stark auf JavaScript setzen, Ergebnisse in ihrem bestehenden Observability-Stack wollen oder Tests aus CI-Pipelines als Release-Gates ausführen müssen.

Locust ist in Python geschrieben, wird in Python geskriptet und bringt eine Web-UI mit, über die auch Nicht-Entwickler Läufe starten und Live-Charts beobachten können. Es ist die pragmatische Wahl, wenn Ihr QA-Team Python schreibt, wenn Sie komplexe Szenariologik wollen (bedingte Verzweigungen, Parsen von Responses zur Steuerung nachfolgender Requests) oder wenn das Test-Framework für Personen außerhalb des Plattform-Teams zugänglich sein muss.

Beide Tools beherrschen Performance-Tests gegen REST-APIs, GraphQL, WebSockets und klassische HTML-Anwendungen. Die Entscheidung sollte vom Skillset der Personen getrieben sein, die die Tests in sechs Monaten warten werden – nicht von theoretischen Durchsatzunterschieden.

Ein minimales, aber nützliches k6-Szenario sieht so aus:

import http from 'k6/http';
import { check, sleep, group } from 'k6';

export const options = {
  stages: [
    { duration: '2m', target: 50 },   // ramp up
    { duration: '10m', target: 200 }, // steady load
    { duration: '2m', target: 500 },  // spike
    { duration: '5m', target: 0 },    // ramp down
  ],
  thresholds: {
    http_req_failed: ['rate<0.01'],
    http_req_duration: ['p(95)<800', 'p(99)<2000'],
  },
};

export default function () {
  group('login and browse', () => {
    const login = http.post(`${__ENV.BASE_URL}/api/login`, {
      email: `user_${__VU}@loadtest.local`,
      password: 'test-password',
    });
    check(login, { 'login 200': (r) => r.status === 200 });

    const token = login.json('token');
    const headers = { Authorization: `Bearer ${token}` };

    http.get(`${__ENV.BASE_URL}/api/dashboard`, { headers });
    sleep(3);
    http.get(`${__ENV.BASE_URL}/api/orders?limit=20`, { headers });
    sleep(5);
  });
}

Die Form zählt genauso viel wie die Zahl: hochfahren, halten, Spike, herunterfahren. Die Thresholds machen aus dem Test einen Pass/Fail-Check, den CI erzwingen kann. Ohne Thresholds ist der Test ein Graph; mit Thresholds ist er ein Gate.

Ergebnisse interpretieren: Perzentile statt Durchschnitte

Die durchschnittliche Antwortzeit ist die falsche Schlagzeilen-Metrik. Ein Service, der für 99 % der Requests in 100 ms antwortet und für das übrige 1 % in 30 Sekunden, hat einen Durchschnitt von 400 ms – sieht gut aus –, während 1 % der Nutzer katastrophale Latenz erleben. Die Metriken, die zählen, sind p50 (Median, das typische Nutzererlebnis), p95 (das langsame Nutzererlebnis), p99 (das langsamste reale Erlebnis) und die Fehlerrate.

Ein gesundes Lasttest-Ergebnis zeigt eine flache p95-Linie, während virtuelle Nutzer hochfahren, eine geringe Aufweitung der Lücke zwischen p50 und p99 unter Spitzenlast und eine Fehlerrate nahe null. Ein fehlschlagendes Ergebnis zeigt eine von drei Formen. Ein Hockeyschläger in p95 um ein bestimmtes Concurrency-Level deutet auf erschöpfte Connection Pools, Worker-Limits oder Thread-Pools hin. Eine steigende Fehlerrate bei stabiler Latenz deutet auf Timeouts, ausfallende Upstream-Abhängigkeiten oder Rate Limits hin. Eine Klippe – Latenz in Ordnung bis zu einer Schwelle, dann schlagen Requests schlicht fehl – deutet meist auf vollständige Sättigung des Event Loops, des FPM-Worker-Pools oder der Queue-Worker hin.

Lassen Sie Tests lang genug laufen, um Garbage-Collection-Pausen, Log-Rotation-Effekte und Cache-Eviction-Muster sichtbar zu machen. Ein zehnminütiger Test übersieht viele Probleme, die erst nach dreißig Minuten auftreten.

Die Engpassmuster, die wir immer wieder sehen

Über Code-Quality-Audits und Performance-Reviews von PHP/Symfony- und Next.js-Anwendungen hinweg tauchen dieselben Engpassmuster mit auffälliger Konsistenz auf. Zu wissen, wo man zuerst hinschaut, spart Tage an Analyse.

Das N+1-Query-Problem im Doctrine ORM ist die häufigste Ursache für PHP-Backend-Verlangsamungen unter Last. Ein Controller, der eine Liste von Bestellungen rendert, von denen jede ein Lazy Loading des Kunden auslöst, der wiederum die Rechnungsadresse lazy-lädt, produziert hunderte Queries für eine einzige Seite. Bei geringem Traffic absorbiert das der Pool; bei hohem Traffic läuft der Connection Pool leer und jeder Request wartet auf eine freie Verbindung. Die Lösung sind explizite fetch='EAGER'-Joins oder Repository-Methoden, die vollständig hydrierte Entities zurückgeben – meist eine Zwei-Zeilen-Änderung.

PHP-FPM-Worker-Erschöpfung ist der zweithäufigste Fehlermodus. Ein Server mit fünfzig konfigurierten FPM-Workern kann nur fünfzig gleichzeitige Requests bedienen. Wenn ein Endpoint vier Sekunden auf einen Upstream-Service wartet, verbrauchen fünfzig gleichzeitige Nutzer dieses Endpoints jeden Worker, und alle anderen Requests stauen sich. Das Symptom ist eine plötzliche Klippe im Lasttest um die Worker-Anzahl herum. Die Lösung ist eine Kombination aus mehr Workern, Verlagerung langsamer Aufrufe in Background-Jobs und Timeouts, damit ein fehlerhafter Upstream nicht den gesamten Pool auffrisst.

Fehlendes oder falsch konfiguriertes Caching ist das dritte. OPcache sollte in Produktion mit großzügigem Speicher und ausreichendem max_accelerated_files aktiviert sein. Symfonys HTTP-Cache, Doctrines Second-Level-Cache und Redis für Session- und Query-Caching sollten alle gezielt lastgetestet werden – viele Teams schalten sie ein und verifizieren nie die Hit-Raten unter realistischer Last.

Datenbankindizes, die bei einer Million Zeilen perfekt sind, sind bei hundert Millionen nutzlos. Der Lasttest sollte gegen einen Datenbestand in der Größe laufen, die Sie in zwölf bis achtzehn Monaten erwarten, nicht in der heutigen. Slow-Query-Logs, die während des Lasttests erfasst werden, zeigen direkt auf die Queries, die mit wachsenden Daten zuerst brechen werden.

Die Sättigung der Background-Job-Queue ist ein leiserer, aber nicht weniger gefährlicher Fehlermodus. Wenn die Web-Schicht Bestellungen schneller annimmt, als Worker sie verarbeiten können, wächst die Queue unbegrenzt. Lasttests, die die Worker-Schicht ignorieren, melden Erfolg, während der Rückstau still anwächst. Nehmen Sie die Queue-Tiefe in Ihre Lasttest-Dashboards auf.

Für Graceful Degradation entwerfen

Jedes System hat einen Bruchpunkt. Das Engineering-Ziel ist nicht, ihn zu eliminieren – das ist unmöglich –, sondern sicherzustellen, dass sein Erreichen nicht in einen Komplettausfall kaskadiert.

Praktische Muster, die den Unterschied machen: Circuit Breaker an jedem Upstream-HTTP-Aufruf, damit eine langsame Abhängigkeit nicht alle Worker verbraucht; Request-Timeouts auf jeder Ebene (HTTP-Client, Datenbank, Cache); Bulkheads, die verhindern, dass ein schwerer Endpoint andere aushungert, typischerweise über separate FPM-Pools oder separate Services; Rate Limiting am Edge, damit ein einzelner Client das System nicht über seine gemessenen Grenzen treiben kann; und Read-only-Fallbacks für Dashboards und Reports, damit Lese-Traffic überlebt, wenn Schreibvorgänge fehlschlagen.

Der Lasttest ist es, der diese Muster validiert. Schalten Sie mitten im Test eine Redis-Instanz ab und beobachten Sie, ob die Anwendung weiter ausliefert oder stirbt. Fügen Sie fünfhundert Millisekunden Latenz beim Payment-Provider hinzu und sehen Sie, ob der Checkout-Endpoint sauber timeoutet oder die Worker erschöpft. Das sind die Fragen, die nur ein kontrollierter Test ehrlich beantworten kann.

Load Testing als kontinuierliche Praxis

Die Teams, die Skalierung gut meistern, führen Lasttests nach Zeitplan aus, nicht nur vor Launches. Ein wöchentlicher Lauf gegen Staging, mit denselben Szenarien und Thresholds, erzeugt eine Trendlinie: Performance verbessert sich, ist stabil oder verschlechtert sich. Bei einer Verschlechterung ist der Commit-Bereich klein und die Ursache leicht zu finden. Läuft der Test nur vor einem großen Launch, ist jede Regression eine Überraschung und jeder Fix entsteht unter Druck.

Lasttests als nächtlichen Job oder Pre-Release-Gate in CI zu integrieren ist eine einmalige Investition mit dauerhaftem Ertrag. k6 Cloud, Grafana k6 und Locust mit Prometheus-Export unterstützen dieses Muster alle out of the box.

Teams, die signifikantes Traffic-Wachstum planen, Enterprise-Onboarding vorbereiten oder sich von einem kürzlichen Incident erholen, profitieren oft von einer externen Perspektive auf ihr Performance-Engineering. Wenn Ihr Team auf einen solchen Moment zusteuert: Wir helfen mittelgroßen europäischen SaaS-Unternehmen, Load-Testing-Programme zu entwerfen und die dabei gefundenen Engpässe zu beheben – als Teil umfassenderer Engagements in individueller Softwareentwicklung und digitaler Transformation. Kontaktieren Sie uns unter hello@wolf-tech.io oder besuchen Sie wolf-tech.io für eine kostenlose Beratung.