GraphQL Rate-Limiting nach Query-Complexity statt Request-Zählung
AI generated
{ }
type
GraphQL · Rate-Limiting · Sicherheit · Cost Analysis
GraphQL Rate-Limiting nach Query-Complexity
warum reine Request-Zählung nicht ausreicht

Ein einzelner GraphQL-Request kann verschachtelte Felder, mehrfache Aliase und tiefe Relationen kombinieren und dabei so viel Datenbank-Last erzeugen wie tausend einfache REST-Anfragen. Wer Rate-Limiting nur über die Anzahl der Requests pro Minute umsetzt, schützt seine API gegen genau diese Angriffe nicht. Query-Complexity-Analyse bewertet jede Anfrage nach ihren tatsächlichen Kosten, bevor sie überhaupt ausgeführt wird.

17 Min. Lesezeit Cost Analysis · Depth Limits · Token Bucket Apollo Server · graphql-cost-analysis · Redis

1. Warum Request-Zählung bei GraphQL scheitert

Klassisches Rate-Limiting bei REST-APIs zählt Requests pro Zeitfenster, etwa hundert Anfragen pro Minute pro API-Key. Diese Annahme funktioniert, weil ein einzelner REST-Endpunkt in etwa immer dieselbe Menge an Arbeit auslöst. GraphQL Rate-Limiting nach demselben Prinzip versagt jedoch, weil ein einzelner GraphQL-Request eine beliebig komplexe Baumstruktur aus verschachtelten Feldern beschreiben kann. Ein Client kann in einer einzigen Anfrage tausend Produkte mit jeweils ihren Bewertungen, Varianten und verwandten Produkten abfragen, was serverseitig hunderte Datenbank-Queries auslöst.

Das macht Request-Zählung zu einem trügerischen Schutz: Ein Angreifer bleibt formal innerhalb des Limits von hundert Requests pro Minute, produziert aber durch geschickt verschachtelte Queries eine Last, die tausend flache REST-Aufrufe verursachen würde. Ohne GraphQL Rate-Limiting auf Basis der tatsächlichen Query-Kosten bleibt die API gegen genau diese Form von Denial-of-Service-Angriffen ungeschützt, selbst wenn ein klassisches Request-Limit korrekt konfiguriert ist.

2. Was Query-Complexity überhaupt misst

Query-Complexity ist eine numerische Bewertung, die vor der Ausführung berechnet wird und abschätzt, wie teuer eine Anfrage für den Server werden würde. Die Grundidee bei GraphQL Rate-Limiting nach Complexity: Jedes Feld im Schema bekommt einen Basis-Kostenwert zugewiesen, meist 1 für ein einfaches Skalarfeld. Felder, die Listen zurückgeben, multiplizieren ihre Kosten mit der erwarteten Anzahl an Elementen, etwa über ein first- oder limit-Argument. Verschachtelte Felder summieren sich, sodass eine tief verschachtelte Query exponentiell teurer wird als eine flache.

Der entscheidende Vorteil gegenüber Request-Zählung: Die Complexity-Berechnung erfolgt vor der Ausführung, rein durch Analyse des Query-Baums gegen das Schema. Der Server muss keine einzige Datenbankabfrage ausführen, um eine überteuerte Query zu erkennen und abzulehnen. Damit schützt GraphQL Rate-Limiting nach Complexity nicht nur vor absichtlichen Angriffen, sondern auch vor versehentlich ineffizienten Queries aus einem schlecht optimierten Frontend.

3. Complexity-Werte im Schema definieren

In der Praxis werden Complexity-Werte über Direktiven oder Konfigurationsobjekte pro Feld im Schema hinterlegt. Standardmäßig vergeben die meisten Bibliotheken einen Basiswert von 1 pro Feld, doch teure Operationen wie Volltextsuche, Aggregationen oder Berechnungen über große Datenmengen sollten explizit höher bewertet werden. Ein Feld wie products(search: String): [Product], das eine Elasticsearch-Abfrage auslöst, verdient realistischerweise einen deutlich höheren Basiswert als ein simples product(id: ID!): Product.

Diese Feinabstimmung ist der Teil von GraphQL Rate-Limiting, der am meisten Domänenwissen erfordert. Ein pauschaler Wert von 1 pro Feld ignoriert, dass manche Resolver einen einzigen Redis-Cache-Hit brauchen, während andere eine teure Aggregation über Millionen Zeilen anstoßen. Teams, die Complexity-Werte einmal setzen und nie wieder anpassen, laufen Gefahr, dass sich die tatsächlichen Resolver-Kosten über die Zeit von den hinterlegten Schätzwerten entfernen, etwa wenn ein Feld nachträglich einen teuren externen API-Call bekommt.


# Schema with explicit complexity weighting via directive
directive @cost(value: Int!, multipliers: [String!]) on FIELD_DEFINITION

type Query {
  product(id: ID!): Product @cost(value: 1)
  products(first: Int = 20, search: String): [Product!]!
    @cost(value: 5, multipliers: ["first"])
  productRecommendations(productId: ID!, limit: Int = 10): [Product!]!
    @cost(value: 20, multipliers: ["limit"])
}

type Product {
  id: ID!
  name: String!
  # Reviews trigger a join, weighted higher than a plain scalar
  reviews(first: Int = 10): [Review!]! @cost(value: 3, multipliers: ["first"])
  relatedProducts(first: Int = 5): [Product!]! @cost(value: 8, multipliers: ["first"])
}

4. Statische Analyse vor der Ausführung

Die technische Umsetzung von GraphQL Rate-Limiting nach Complexity läuft als eigene Validierungsphase, bevor der Query-Executor überhaupt einen Resolver aufruft. Bibliotheken wie graphql-cost-analysis für Apollo Server oder das eingebaute Complexity-Plugin in GraphQL Yoga durchlaufen den geparsten Query-AST, summieren die konfigurierten Kosten pro Feld und berücksichtigen dabei Argumente wie first oder limit als Multiplikatoren. Überschreitet die Summe einen konfigurierten Schwellenwert, wird die Anfrage abgelehnt, bevor auch nur ein Resolver ausgeführt wurde.

Wichtig ist, Variablen korrekt in die Berechnung einzubeziehen: Ein Angreifer könnte versuchen, das Limit-Argument über eine Variable statt eines Literals zu übergeben, um eine statische Codeanalyse zu umgehen. Ausgereifte GraphQL Rate-Limiting-Implementierungen lösen Variablen vor der Complexity-Berechnung auf und verwenden bei fehlenden Variablenwerten konservative Höchstwerte, statt die Multiplikation einfach zu überspringen.


// server.js — enforcing complexity-based rate limiting in Apollo Server
import { costAnalysis } from 'graphql-cost-analysis';
import { ApolloServer } from '@apollo/server';

const MAX_COMPLEXITY = 1000;

const server = new ApolloServer({
  schema,
  plugins: [
    {
      async requestDidStart() {
        return {
          async didResolveOperation({ request, document, contextValue }) {
            const complexity = costAnalysis({
              document,
              schema,
              variables: request.variables ?? {},
              // Conservative default when a client omits a limit argument
              defaultCost: 1,
              maximumCost: MAX_COMPLEXITY,
            });

            if (complexity > MAX_COMPLEXITY) {
              throw new GraphQLError('Query too complex', {
                extensions: {
                  code: 'QUERY_TOO_COMPLEX',
                  complexity,
                  maxComplexity: MAX_COMPLEXITY,
                },
              });
            }

            // Track remaining budget for this client in Redis
            await contextValue.rateLimiter.consume(contextValue.clientId, complexity);
          },
        };
      },
    },
  ],
});

5. Complexity-Budgets pro Client und Zeitfenster

Eine einmalige Prüfung pro Query reicht nicht aus, denn ein Client könnte viele mittelgroße Queries statt einer einzigen riesigen senden und trotzdem dieselbe Gesamtlast erzeugen. Wirksames GraphQL Rate-Limiting kombiniert daher die Complexity-Bewertung pro Query mit einem Budget pro Client über ein rollierendes Zeitfenster, ähnlich einem Token-Bucket-Algorithmus. Jeder Client bekommt ein Kontingent an Complexity-Punkten pro Minute zugewiesen, das mit jeder Anfrage abgebaut wird und sich kontinuierlich wieder auffüllt.

Redis eignet sich gut als zentraler Speicher für diese Budgets, da atomare Increment-Operationen über INCRBY und Ablaufzeiten über EXPIRE race-condition-frei funktionieren, selbst wenn mehrere Server-Instanzen gleichzeitig Anfragen verarbeiten. Ein Client, der sein Budget aufbraucht, bekommt einen klaren Fehler mit Angabe, wann das Budget sich wieder auffüllt, statt einfach willkürlich Anfragen abzulehnen. Diese Kombination aus Query-Complexity und Zeitfenster-Budget ist der eigentliche Kern von produktivem GraphQL Rate-Limiting.

6. Depth Limits als ergänzende Schutzschicht

Neben Complexity-Scores ist ein einfaches Depth Limit eine sinnvolle Ergänzung, die viele Angriffsvektoren bereits vor der eigentlichen Kostenberechnung abfängt. Zirkuläre Beziehungen im Schema, etwa Product.relatedProducts.relatedProducts.relatedProducts, ermöglichen theoretisch unbegrenzt tiefe Verschachtelung. Ein hartes Limit von etwa sieben bis zehn Verschachtelungsebenen verhindert solche pathologischen Queries unabhängig von der Complexity-Berechnung und ist deutlich einfacher zu implementieren als vollständige Cost Analysis.

Depth Limits ersetzen aber kein vollständiges GraphQL Rate-Limiting nach Complexity, weil eine flache Query mit vielen breiten Listen ebenso teuer sein kann wie eine tiefe. Die robusteste Architektur kombiniert beide Schutzschichten: Depth Limits als schnelle, günstige Vorprüfung direkt nach dem Parsen, Complexity-Analyse als genauere zweite Schicht, die tatsächliche Kosten inklusive Listen-Multiplikatoren berücksichtigt.


# Example: rejecting a query before execution based on combined limits
curl -X POST https://api.mironsoft.de/graphql \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"query": "{ products(first: 100) { relatedProducts(first: 50) { relatedProducts(first: 50) { name } } } } "}'

# Response when complexity budget is exceeded
# HTTP/1.1 200 OK (GraphQL errors are returned with 200, not 429)
# {
#   "errors": [{
#     "message": "Query too complex",
#     "extensions": { "code": "QUERY_TOO_COMPLEX", "complexity": 2500, "maxComplexity": 1000 }
#   }]
# }

7. Fehlerantworten und Client-Kommunikation

Anders als bei REST, wo ein überschrittenes Limit klassisch mit HTTP-Status 429 samt Retry-After-Header signalisiert wird, antwortet GraphQL bei den meisten Servern grundsätzlich mit HTTP 200, auch bei Fehlern, da Teilergebnisse neben Fehlern koexistieren können. GraphQL Rate-Limiting-Fehler landen deshalb im errors-Array der Antwort, mit einem strukturierten extensions-Objekt, das Code, aktuelle Complexity und Limit enthält, damit Client-Bibliotheken programmatisch reagieren können, statt nur eine generische Fehlermeldung anzuzeigen.

Für ein gutes Client-Erlebnis lohnt es sich, den verbleibenden Budget-Stand proaktiv mitzuliefern, etwa über einen benutzerdefinierten Header wie X-RateLimit-Remaining-Complexity. So kann ein Frontend-Team schon vor dem eigentlichen Limit warnen oder eine Anfrage clientseitig vereinfachen, etwa durch Reduzieren der angefragten Listen-Größe, statt erst nach einem harten Fehler zu reagieren. Gute Dokumentation der Complexity-Kosten pro Feld gehört ebenfalls zu einem sauber kommunizierten GraphQL Rate-Limiting-System.


{
  "errors": [
    {
      "message": "Query too complex",
      "extensions": {
        "code": "QUERY_TOO_COMPLEX",
        "complexity": 2500,
        "maxComplexity": 1000
      }
    }
  ],
  "extensions": {
    "rateLimit": {
      "remainingComplexity": 0,
      "resetAt": "2026-08-06T14:05:00Z"
    }
  }
}

8. Typische Stolperfallen bei der Einführung

Der häufigste Fehler beim Einführen von GraphQL Rate-Limiting nach Complexity ist ein zu niedrig angesetztes Limit, das legitime, aber datenintensive Dashboards blockiert, etwa Admin-Oberflächen, die bewusst viele verschachtelte Felder auf einmal laden. Die Lösung ist selten ein pauschal höheres Limit für alle, sondern differenzierte Budgets nach Client-Typ: interne Admin-Tools bekommen ein höheres Kontingent als öffentliche Storefront-Clients.

Ein zweiter Fehler ist das Ignorieren von Fragment-Wiederverwendung: Wird dasselbe Fragment mehrfach in einer Query über Aliase eingebunden, muss die Complexity-Berechnung jede Instanz einzeln zählen, sonst lässt sich das Limit über wiederholte Aliase gezielt umgehen. Ein dritter, subtiler Fehler betrifft Introspection-Queries: Diese sollten von der Complexity-Berechnung ausgenommen oder mit einem eigenen, moderaten Limit versehen werden, da Tools wie GraphiQL sonst durch das reguläre Schema-Introspection-Query bereits das Budget sprengen können.


# Alias abuse — without per-instance counting this could bypass the limit
query AliasAbuse {
  p1: products(first: 50) { relatedProducts(first: 50) { name } }
  p2: products(first: 50) { relatedProducts(first: 50) { name } }
  p3: products(first: 50) { relatedProducts(first: 50) { name } }
  # Correct cost analysis multiplies cost by the number of alias instances,
  # so this query costs 3x a single products(first: 50) call, not 1x
}

9. Rate-Limiting-Strategien im Vergleich

Die folgende Übersicht vergleicht die gängigen Ansätze für GraphQL Rate-Limiting nach Schutzwirkung, Implementierungsaufwand und Nebenwirkungen für legitime Clients.

Strategie Schutz gegen teure Queries Implementierungsaufwand Risiko für legitime Clients
Request-Zählung Gering Sehr niedrig Niedrig
Depth Limits Mittel Niedrig Mittel
Query-Complexity-Analyse Hoch Mittel bis hoch Niedrig (bei guter Kalibrierung)
Time-Based Complexity Analysis Sehr hoch Hoch Niedrig

Time-Based Complexity Analysis misst zusätzlich tatsächliche Resolver-Laufzeiten und passt Kostenwerte automatisch an reale Latenzen an, statt sich auf statisch geschätzte Werte zu verlassen. Für die meisten Teams ist eine gut kalibrierte statische Query-Complexity-Analyse kombiniert mit Depth Limits der beste Kompromiss aus Schutzwirkung und Wartungsaufwand.

Mironsoft

GraphQL-Sicherheit, API-Architektur und Magento-Integrationen

GraphQL-API gegen teure Queries wirklich abgesichert?

Wir analysieren euer Schema, definieren realistische Complexity-Werte pro Feld und richten Rate-Limiting mit Redis-Budgets ein, das legitime Clients nicht ausbremst.

Schema-Audit

Identifikation teurer Felder und realistischer Complexity-Kosten pro Resolver

Limiting-Setup

Cost Analysis, Depth Limits und Redis-basierte Budgets produktiv einrichten

Client-Kommunikation

Strukturierte Fehlerantworten und Budget-Header für ein sauberes API-Erlebnis

10. Zusammenfassung

GraphQL Rate-Limiting nach Query-Complexity statt reiner Request-Zählung ist keine Optimierung, sondern eine Notwendigkeit, sobald eine API produktiv erreichbar ist. Weil GraphQL erlaubt, in einer einzigen Anfrage beliebig verschachtelte und beliebig breite Datenstrukturen zu beschreiben, ist die Anzahl der Requests kein verlässlicher Indikator für die tatsächliche Serverlast. Cost Analysis bewertet jede Query vor der Ausführung nach ihren echten Kosten und lehnt überteuerte Anfragen ab, bevor auch nur ein Resolver läuft.

Die robusteste Umsetzung kombiniert statische Complexity-Werte im Schema, Depth Limits als schnelle Vorprüfung und ein Client-Budget über ein rollierendes Zeitfenster in Redis. Wer diese Bausteine sorgfältig kalibriert und Fehlerantworten strukturiert kommuniziert, schützt seine GraphQL-API gegen Denial-of-Service-Angriffe und versehentlich ineffiziente Frontend-Queries gleichermaßen, ohne legitime Nutzer unnötig auszubremsen.

GraphQL Rate-Limiting nach Query-Complexity — Das Wichtigste auf einen Blick

Request-Zählung reicht nicht

Eine einzelne verschachtelte Query kann tausend REST-Aufrufe an Datenbank-Last erzeugen, ohne das Request-Limit zu überschreiten.

Cost Analysis vor Ausführung

Complexity wird aus dem Query-AST berechnet, bevor ein Resolver läuft. Überteuerte Queries werden sofort abgelehnt.

Depth Limits als Vorprüfung

Ein hartes Verschachtelungslimit fängt pathologische Queries günstig ab, bevor die genauere Cost Analysis läuft.

Budgets pro Client in Redis

Rollierende Zeitfenster-Budgets verhindern, dass viele mittelgroße Queries dieselbe Last wie eine große erzeugen.

11. FAQ: GraphQL Rate-Limiting nach Query-Complexity

1Warum reicht Request-Zählung nicht?
Ein Request kann durch Verschachtelung beliebig viel Serverlast erzeugen, unabhängig von der Anzahl an Requests.
2Wie wird Complexity berechnet?
Basiskosten pro Feld, multipliziert mit Listen-Größen, summiert über die gesamte verschachtelte Struktur.
3Vor oder nach Ausführung geprüft?
Immer davor, rein durch Analyse des Query-Baums gegen das Schema, ohne Resolver-Aufruf.
4Welcher HTTP-Status bei Überschreitung?
Meist weiterhin 200, der Fehler steckt im errors-Array mit strukturierten extensions.
5Reichen Depth Limits allein?
Nein, flache breite Queries können genauso teuer sein. Immer mit vollständiger Complexity-Analyse kombinieren.
6Wie Admin-Dashboards nicht blockieren?
Differenzierte Budgets nach Client-Typ statt einem pauschalen Limit für alle.
7Umgehung über Aliase möglich?
Nicht bei korrekter Implementierung, jede Alias-Instanz muss einzeln in die Complexity einfließen.
8Introspection-Queries mitzählen?
Besser mit eigenem moderaten Limit oder ganz ausnehmen, sonst sprengen Tools wie GraphiQL das Budget.
9Wo Budgets speichern?
Meist in Redis mit atomaren Increment- und Expire-Operationen, race-condition-frei über mehrere Instanzen.
10Was ist Time-Based Complexity Analysis?
Eine erweiterte Cost Analysis, die reale Resolver-Laufzeiten misst und Kosten dynamisch anpasst.