GraphQL-API-Lasttest: ein praktischer Leitfaden
6 Min. Lesezeit
Für einen GraphQL-API-Lasttest skripten Sie die Operationen, die Ihre Clients tatsächlich senden, geben jeder im Testtool einen eigenen Namen und mischen sie mit wechselnden Variablen im selben Verhältnis wie in der Produktion. Prüfen Sie jeden Antwort-Body auf ein errors-Array, denn ein GraphQL-Server antwortet meist mit HTTP 200, auch wenn ein Teil der Operation fehlgeschlagen ist. Steigern Sie dann die Last und beurteilen Sie jede Operation nach ihren eigenen Perzentilen.
Quellen: graphql.org, der Entwurf der GraphQL-over-HTTP-Spezifikation und die Dokumentation der jeweiligen Tools, alles geprüft am 9. Oktober 2026.
| Tool | Operation benennen mit | Antwort mit errors scheitern lassen | Variablen einspeisen aus |
|---|---|---|---|
| k6 | tags: { name: "..." } | Ein check() auf den geparsten Body | SharedArray |
| JMeter | Dem Namen des Samplers | JSON Assertion auf $.errors, invertiert | CSV Data Set Config |
| Gatling | http("...") oder dem GraphQL-Plugin | jsonPath("$.errors").notExists() | Feedern |
| Artillery | Einem name an der Anfrage | notHasProperty: "errors" | config.payload |
Warum braucht eine GraphQL-API einen anderen Lasttest?
Weil jede Operation an dieselbe URL geht und die Arbeit dahinter von der Abfrage und ihren Variablen abhängt. Ein nach URL gruppierter Bericht wirft einen billigen Lookup und eine teure Suche in denselben Topf.
Laut dem Leitfaden Serving over HTTP von graphql.org arbeitet ein GraphQL-Server über einen einzigen Endpunkt, meist /graphql. Operationen kommen dort per POST an, mit einem JSON-Body aus query, einem optionalen operationName und variables.
Wie erhalten Sie Ergebnisse pro Operation?
Benennen Sie jede Anfrage nach ihrer GraphQL-Operation und lassen Sie das Tool seine Statistiken nach diesem Namen gruppieren. Jedes Tool hat dafür einen eigenen Schalter (geprüft am 9. Oktober 2026).
- k6: Der Leitfaden zu Tags und Gruppen empfiehlt
tags.name, um einer Anfrage einen sprechenden Namen zu geben, und Thresholds können auf einen einzelnen Tag zielen, etwahttp_req_duration{name:GetProduct}. - JMeter: Der Aggregate Report legt für jede unterschiedlich benannte Anfrage eine eigene Zeile an. Seit JMeter 5.4 hat der Sampler GraphQL HTTP Request eigene Felder für Query, Variables und Operation Name.
- Gatling: Der Name der Anfrage ist der Schlüssel für die Statistiken, und das GraphQL-Plugin benennt Anfragen nach der Operation, etwa
query GetUser. - Artillery: Das Plugin metrics-by-endpoint berichtet pro URL und ergänzt den Eintrag um den
nameeiner Anfrage, sodass benannte Operationen getrennt erscheinen;useOnlyRequestNames: truelässt die URL weg und behält nur den Namen.
Welche Operationen sollte der Test senden?
Die, die auch die Produktion erhält, im selben Verhältnis. Entnehmen Sie die Häufigkeit jeder Operation Ihren Server-Logs oder Ihrem GraphQL-Monitoring und machen Sie daraus einen gewichteten Mix aus Lesezugriffen und Mutationen.
Verwenden Sie die Dokumente, die Ihre Clients ausliefern, in ihrer echten Tiefe, statt einer vereinfachten Abfrage. Speisen Sie Variablen aus einer Datei ein, damit der Server viele verschiedene Datensätze nachschlägt, statt ein einziges Ergebnis aus dem Cache zu liefern. Halten Sie auch die Zahl unterschiedlicher Operationen realistisch: Apollos Technote zum Lasttest von GraphQL, ein Herstellerdokument, erklärt, dass jede neue Operation vor der Ausführung geparst und validiert wird, und rät davon ab, weit mehr eindeutige Operationen zu erzeugen, als echte Clients senden.
Mutationen verändern Daten, führen Sie sie also dort aus, wo sich die Daten zurücksetzen lassen. Die Schritt-für-Schritt-Anleitung zum Lasttest behandelt Ziele und Ramp-up.
Warum kann eine Antwort mit HTTP 200 trotzdem ein Fehler sein?
Weil GraphQL Fehler im Antwort-Body meldet. Eine Anfrage kann Status 200 mit einem errors-Array und unvollständigen Daten liefern, und eine reine Statusprüfung lässt sie durch.
Die Seite Response auf graphql.org erklärt, dass die Ausführung weiterläuft, wenn ein Resolver fehlschlägt, und die Antwort dann data neben errors enthält. Der HTTP-Leitfaden ergänzt, dass eine solche Antwort trotzdem einen 2xx-Status erhalten soll, weil HTTP keinen Code für einen Teilerfolg kennt, und dass manche älteren Server sogar bei Validierungsfehlern mit 2xx antworten. Der Entwurf GraphQL over HTTP schlägt für Teilerfolge mit dem neueren Medientyp application/graphql-response+json einen eigenen Status 294 vor.
Im Lasttest schadet das doppelt. Fehler werden nicht gezählt, und Anfragen, die schnell scheitern, etwa an einem Tiefenlimit abgelehnte Operationen, drücken die Antwortzeiten nach unten.
Wie prüfen Sie das errors-Array in jedem Tool?
Parsen Sie den JSON-Body und lassen Sie die Anfrage scheitern, wenn errors vorhanden ist. Jedes Tool bringt dafür ein eingebautes Mittel mit.
In k6 erledigt das ein Check auf den geparsten Body, und ein Threshold auf die Metrik checks lässt den Lauf scheitern, denn Checks allein ändern den Exit-Code von k6 nicht:
import http from "k6/http";
import { check, sleep } from "k6";
import { SharedArray } from "k6/data";
const URL = "https://staging.example.com/graphql";
const ids = new SharedArray("ids", () =>
JSON.parse(open("./product-ids.json")));
const QUERY = `query GetProduct($id: ID!) {
product(id: $id) { id name price }
}`;
export const options = {
vus: 20,
duration: "5m",
thresholds: {
checks: ["rate>0.99"],
"http_req_duration{name:GetProduct}": ["p(95)<400"],
},
};
function parse(res) {
try { return res.json(); } catch (e) { return null; }
}
export default function () {
const id = ids[Math.floor(Math.random() * ids.length)];
const payload = JSON.stringify({
query: QUERY,
operationName: "GetProduct",
variables: { id: id },
});
const res = http.post(URL, payload, {
headers: { "Content-Type": "application/json" },
tags: { name: "GetProduct" },
});
const body = parse(res);
check(res, {
"status is 2xx": (r) => r.status >= 200 && r.status < 300,
"no GraphQL errors": () => body !== null && !body.errors,
});
sleep(1);
}
SharedArray lädt die IDs einmal für alle virtuellen Benutzer. Das try verhindert, dass ein Body, der kein JSON ist, etwa die Fehlerseite eines Proxys, die Iteration abbricht.
In JMeter fügen Sie eine JSON Assertion mit dem Pfad $.errors hinzu und setzen den Haken bei “Invert assertion”, damit das Sample scheitert, sobald der Pfad existiert. Ein JSON Extractor übernimmt IDs in Variablen für die nächste Operation.
In Gatling ergänzen Sie .check(jsonPath("$.errors").notExists()), beides aus der Check-Referenz. Das GraphQL-Plugin prüft Status und Fehler standardmäßig, doch seine Setup-Seite begrenzt Läufe mit der Community Edition auf 5 Benutzer und 5 Minuten (geprüft am 9. Oktober 2026). Für größere Tests ohne Gatling Enterprise nehmen Sie normale HTTP-Anfragen mit diesem Check.
In Artillery bietet das expect-Plugin notHasProperty: "errors", und reportFailuresAsErrors: true bringt fehlgeschlagene Erwartungen in den Bericht. Ein capture mit einem json-Pfad trägt einen Wert in die nächste Anfrage.
Worauf sollten Sie neben den Antwortzeiten achten?
Auf Caching, Persisted Queries und die Datenbanklast pro Anfrage. Jedes davon kann einen Test schneller oder langsamer als die Produktion machen, aus Gründen, die mit der Kapazität nichts zu tun haben.
Der Performance-Leitfaden von graphql.org hält fest, dass per GET gesendete Abfragen HTTP- und CDN-Caching nutzen können und dass Persisted Queries einen Client statt des vollständigen Dokuments einen Hash senden lassen. Machen Ihre Clients das, sollte der Test es auch tun. Machen sie es nicht, misst ein Test, der eine Abfrage mit immer denselben Variablen wiederholt, einen Cache, den echter Traffic verfehlt. Apollos Technote ergänzt, dass Apollo Server, Gateway und der GraphOS Router Automatic Persisted Queries standardmäßig aktivieren.
Derselbe Leitfaden beschreibt das N+1-Problem: Eine Liste, deren Resolver jedes Element einzeln lädt, macht aus einer Anfrage viele Datenbankabfragen, meist behoben durch Batching mit einem Tool wie DataLoader. Beobachten Sie Datenbankabfragen pro Sekunde neben Anfragen pro Sekunde; steigen sie unter wachsender Last viel schneller, ist ein Resolver der Verdächtige. Auch Tiefenlimits, Komplexitätslimits und Rate Limiting können während eines Tests greifen.
Woher sollte die Last kommen?
Von einem Rechner, solange Sie das Skript bauen und einen Smoke-Test fahren, dann aus der Cloud, wenn Sie mehr Last oder Traffic aus mehreren Regionen brauchen.
LoadFocus führt k6-Skripte aus 25 Cloud-Regionen aus. Dort kommen die Zahl der Benutzer und die Dauer aus den Testeinstellungen statt aus den options des Skripts, und Arrival-Rate-Szenarien laufen mit einer festen Zahl von Benutzern statt mit einer festen Anfragerate, der Unterschied, den der Artikel zum offenen und geschlossenen Lastmodell erklärt. LoadFocus veröffentlicht außerdem eine Vorlage für GraphQL-Endpunkte mit komplexen Abfragen (auf Englisch).
FAQ
Sollte ein GraphQL-Lasttest Abfragen per GET oder per POST senden? So, wie es Ihre Clients tun. graphql.org erlaubt GET nur für Abfragen, Mutationen müssen POST verwenden. Sendet die Produktion Persisted Queries per GET über ein CDN, testen Sie auch diesen Weg.
Kann dasselbe Skript auch GraphQL-Subscriptions unter Last setzen? Meist nicht. graphql.org weist darauf hin, dass Subscriptions langlebig sind und oft über WebSockets oder Server-Sent Events laufen, sie brauchen also ein eigenes Szenario in einem Tool, das dieses Transportprotokoll unterstützt.
Sollte eine Teilantwort als fehlgeschlagene Anfrage zählen? In den meisten Tests ja, denn der Client hat für einen Teil seiner Anfrage einen Fehler bekommen. Sind manche Fehler erwartet, etwa nicht gefundene Ergebnisse für gelöschte Datensätze, prüfen Sie gezielt auf diese Fehlercodes und lassen Sie den Rest scheitern.
Weiterlesen
- Lasttest durchführen: eine Schritt-für-Schritt-Anleitung
- Lasttests in GitHub Actions: ein praktischer Leitfaden
- WebSocket-Lasttest-Tools: sechs Optionen im Vergleich
- gRPC-Lasttest: Tools, Einrichtung und Kennzahlen
Die technischen Aussagen in diesem Artikel wurden nicht unabhängig geprüft.