Field-Level Cost Analysis: teure Felder in GraphQL gezielt limitieren
AI generated
{ }
type
GraphQL · Query Complexity · API-Sicherheit · Performance
Field-Level Cost Analysis
teure Felder gezielt limitieren

Ein einzelnes verschachteltes Feld mit teurem Datenbank-Join kann eine ganze Query unbrauchbar langsam machen, während pauschale Tiefen-Limits davon nichts mitbekommen. Field-Level Cost Analysis rechnet jedem Feld im Schema einen konkreten Kostenwert zu und macht Budgets pro Query und pro Client durchsetzbar, statt nur die Schachtelungstiefe zu zählen.

17 Min. Lesezeit graphql-cost-analysis · Kosten-Direktiven · Query-Budgets Node.js · TypeScript · Apollo Server

1. Warum Query-Complexity-Limits allein nicht reichen

Ein simples Tiefen-Limit zählt nur, wie viele Ebenen eine Query verschachtelt ist, unabhängig davon, was jedes Feld tatsächlich kostet. Eine Query mit drei Ebenen kann trivial sein, wenn jedes Feld nur einen einzigen Datenbank-Lookup auslöst, oder katastrophal teuer, wenn ein einziges Feld einen vollständigen Volltextindex durchsucht. Field-Level Cost Analysis löst genau dieses Problem, indem sie jedem Feld im Schema einen individuellen Kostenwert zuweist, statt alle Felder gleich zu behandeln.

Der Unterschied wird besonders bei Aggregations-Feldern deutlich: totalRevenue auf einem Store-Typ kann intern über Millionen Bestellzeilen summieren, während name auf demselben Typ ein einfacher Spaltenzugriff ist. Ein reines Tiefen- oder Knoten-Limit behandelt beide Felder identisch, obwohl der Kostenunterschied um Größenordnungen auseinanderliegt. Field-Level Cost Analysis macht diesen Unterschied explizit und messbar.

In der Praxis zeigt sich der Bedarf meist erst, wenn ein einzelner Client eine unscheinbare Query mit einem teuren, tief verschachtelten Aggregations-Feld abschickt und der Datenbank-Server für Sekunden blockiert. Nachträgliches Debugging ist mühsam, weil die Query auf den ersten Blick harmlos aussieht. Ein proaktives Kostenmodell verhindert dieses Szenario, bevor es in Produktion auftritt.

2. Kosten-Direktiven im Schema definieren

Der übliche Ansatz für Field-Level Cost Analysis ist eine Schema-Direktive wie @cost, die direkt am Feld im SDL steht. Jedes Feld bekommt so seinen Basiswert direkt neben seiner Definition, statt in einer separaten, schwer zu pflegenden Konfigurationsdatei zu verschwinden. Das macht Kosten für jeden im Team sichtbar, der das Schema liest, ohne zusätzliche Dokumentation zu durchsuchen.


# schema.graphql — cost directives make expensive fields explicit
directive @cost(
  complexity: Int!
  multipliers: [String!]
) on FIELD_DEFINITION

type Store {
  id: ID!
  name: String! @cost(complexity: 1)
  # Aggregating millions of order rows is far more expensive than a column read
  totalRevenue(from: String, to: String): Float! @cost(complexity: 50)
  products(limit: Int = 20): [Product!]! @cost(complexity: 5, multipliers: ["limit"])
}

type Product {
  id: ID!
  title: String! @cost(complexity: 1)
  # Recommendation lookups call an external ML service
  recommendations(limit: Int = 10): [Product!]! @cost(complexity: 20, multipliers: ["limit"])
}

Die Direktive multipliers ist entscheidend, weil sie Kosten mit Argumenten verknüpft: Ein Feld mit limit: 5 kostet deutlich weniger als dasselbe Feld mit limit: 500. Ohne diesen Mechanismus müsste jedes Listen-Feld pauschal mit dem Worst-Case-Wert bepreist werden, was kleine, günstige Queries unnötig einschränkt.

3. Cost-Berechnung implementieren

Für die tatsächliche Berechnung existieren fertige Bibliotheken wie graphql-cost-analysis oder graphql-query-complexity, die als Validierungsregel in den GraphQL-Server eingehängt werden. Beide lesen die Kosten-Direktiven aus dem Schema, laufen über den geparsten Query-AST und summieren die Kosten aller angefragten Felder, bevor überhaupt ein Resolver ausgeführt wird. Das ist der zentrale Vorteil von Field-Level Cost Analysis: Die Ablehnung passiert vor der Ausführung, nicht erst, wenn die Datenbank bereits überlastet ist.


// cost-plugin.ts — reject queries above the cost budget before execution
import { createComplexityLimitRule } from 'graphql-validation-complexity'
import type { ApolloServerPlugin } from '@apollo/server'

const MAX_QUERY_COST = 1000

export const costLimitPlugin: ApolloServerPlugin = {
  async requestDidStart() {
    return {
      async didResolveOperation({ request, document, schema }) {
        const cost = calculateCost(schema, document, request.variables ?? {})
        if (cost > MAX_QUERY_COST) {
          throw new Error(
            `Query cost ${cost} exceeds the maximum allowed cost of ${MAX_QUERY_COST}`
          )
        }
      },
    }
  },
}

Der Hook didResolveOperation läuft, nachdem die Query geparst und validiert, aber bevor sie ausgeführt wurde. Wird die Kostenschwelle überschritten, bricht der Server sofort mit einem klaren Fehler ab. Ressourcen der Datenbank oder externer Services werden dadurch nie belastet, was Field-Level Cost Analysis effektiv präventiv statt reaktiv macht.

4. Dynamische Kosten: Multiplikatoren für Listen und Pagination

Statische Kosten reichen nicht aus, sobald Listen und Pagination ins Spiel kommen. Ein Feld products(limit: Int) muss unterschiedliche Kosten je nach angefragtem limit-Wert erzeugen, sonst lässt sich entweder limit: 5 unnötig einschränken oder limit: 10000 viel zu billig durchwinken. Field-Level Cost Analysis berechnet die Gesamtkosten deshalb dynamisch als Basiskosten multipliziert mit dem tatsächlichen Argumentwert.

Verschachtelte Listen verstärken diesen Effekt zusätzlich: Fragt eine Query stores { products(limit: 50) { recommendations(limit: 20) } } } ab, multiplizieren sich die Kosten über mehrere Ebenen hinweg. Eine korrekte Implementierung von Field-Level Cost Analysis muss diese Multiplikation über die gesamte Verschachtelungstiefe propagieren, nicht nur auf der direkten Elternebene rechnen.


// cost-calculator.ts — multiply nested list costs across the whole query tree
function calculateFieldCost(
  fieldCost: number,
  args: Record<string, unknown>,
  multiplierArgs: string[],
  parentMultiplier: number
): number {
  const localMultiplier = multiplierArgs.reduce((acc, argName) => {
    const value = args[argName]
    return typeof value === 'number' ? acc * value : acc
  }, 1)

  // Nested lists compound: 50 products x 20 recommendations = 1000x base cost
  return fieldCost * localMultiplier * parentMultiplier
}

// Example: products(limit: 50).recommendations(limit: 20)
// base cost 20, local multiplier 20, parent multiplier from products = 50
// total = 20 * 20 * 50 = 20000

Ohne diese kaskadierende Multiplikation würde eine tief verschachtelte Query mit mehreren mittelgroßen Limits fälschlich als billig eingestuft, obwohl die tatsächliche Anzahl an Datenbank-Zugriffen exponentiell wächst. Genau diese Fälle sind es, die reale Produktionssysteme überlasten, wenn sie unentdeckt bleiben.

5. Teure Felder markieren: Joins, externe Calls, Aggregationen

Nicht jedes teure Feld ist auf den ersten Blick erkennbar. Ein Feld, das nach außen wie ein einfacher Getter aussieht, kann intern einen externen API-Call an einen Machine-Learning-Service auslösen, der mehrere hundert Millisekunden dauert. Field-Level Cost Analysis zwingt Entwickler dazu, sich bei jedem neuen Feld bewusst zu fragen, was der zugehörige Resolver tatsächlich tut, bevor ein Kostenwert vergeben wird.

Eine sinnvolle Faustregel: Felder mit direktem Spaltenzugriff bekommen Kosten von 1, Felder mit einem zusätzlichen Datenbank-Join oder einer einzelnen externen Anfrage Kosten zwischen 5 und 20, und Felder mit Aggregationen über große Datenmengen oder mehreren verketteten externen Aufrufen Kosten ab 50 aufwärts. Diese Einteilung sollte im Code-Review konsequent geprüft werden, damit neue Felder nicht versehentlich mit einem zu niedrigen Standardwert einsortiert werden.

6. Budget pro Client: interne vs. öffentliche Nutzer

Nicht jeder Client sollte dasselbe Kostenbudget erhalten. Ein internes Reporting-Tool, das über Nacht Batch-Queries ausführt, darf ein deutlich höheres Limit haben als eine mobile App, die im Sekundentakt kleine Queries an dieselbe API schickt. Field-Level Cost Analysis lässt sich problemlos mit einem clientabhängigen Budget kombinieren, indem der maximale Kostenwert aus dem Auth-Kontext statt aus einer globalen Konstante gelesen wird.


// per-client cost budgets based on API key tier
const COST_BUDGETS: Record<string, number> = {
  internal_reporting: 50000,
  mobile_app: 500,
  partner_api: 2000,
  default: 300,
}

function getCostBudget(clientId: string): number {
  return COST_BUDGETS[clientId] ?? COST_BUDGETS.default
}

Dieses Modell erlaubt es, großzügige Budgets für vertrauenswürdige interne Clients zu vergeben, während öffentliche oder wenig überprüfte Clients strikter limitiert werden. Der Aufwand für diese Unterscheidung ist gering, sobald die grundlegende Field-Level Cost Analysis-Infrastruktur einmal steht, weil nur eine zusätzliche Lookup-Tabelle nötig ist.

7. Monitoring: welche Felder die Kosten treiben

Kostenwerte im Schema sind Schätzungen, keine gemessenen Fakten. Ohne Monitoring bleibt unklar, ob die vergebenen Kosten der Realität entsprechen oder ob ein als billig eingestuftes Feld in Wahrheit die Datenbank belastet. Ein sinnvolles Setup loggt für jede Query sowohl die berechnete Kosten-Summe als auch die tatsächliche Ausführungszeit, sodass Abweichungen im Nachhinein sichtbar werden.

Werden diese Daten in ein Dashboard wie Grafana überführt, lassen sich Felder identifizieren, deren reale Latenz nicht zu ihrem geschätzten Kostenwert passt. Solche Ausreißer sind ein starkes Signal dafür, die Kosten-Direktive im Schema nachzujustieren. Field-Level Cost Analysis ist damit kein einmaliges Setup, sondern ein iterativer Prozess, der sich mit wachsender Nutzung der API verfeinert.

8. Fehlerausgabe: verständliche Rejections

Ein abgelehnter Request ohne verständliche Fehlermeldung frustriert Frontend-Entwickler, die nicht wissen, welches Feld für die Ablehnung verantwortlich war. Eine gute Implementierung von Field-Level Cost Analysis gibt nicht nur die Gesamtkosten und das Limit zurück, sondern idealerweise auch die teuersten Einzelfelder, damit der Client gezielt optimieren kann, statt raten zu müssen.


{
  "errors": [
    {
      "message": "Query cost 1450 exceeds the maximum allowed cost of 1000",
      "extensions": {
        "code": "QUERY_COST_EXCEEDED",
        "cost": 1450,
        "maxCost": 1000,
        "expensiveFields": [
          { "field": "stores.products.recommendations", "cost": 1000 },
          { "field": "stores.totalRevenue", "cost": 300 }
        ]
      }
    }
  ]
}

Diese Struktur folgt der GraphQL-Konvention, zusätzliche maschinenlesbare Informationen im extensions-Feld zu transportieren, statt sie in den menschenlesbaren message-Text zu quetschen. Frontend-Teams können den code programmatisch auswerten und dem Nutzer eine passende Meldung anzeigen, während Entwickler die expensiveFields-Liste zum gezielten Debugging nutzen.

9. Field-Level Cost Analysis im Vergleich zu Depth-Limits

Depth-Limits, Node-Limits und Rate-Limiting sind einfachere, aber ungenauere Alternativen zu Field-Level Cost Analysis. Jeder Ansatz schützt vor einer anderen Klasse von Angriffen oder Fehlkonfigurationen, mit unterschiedlichem Implementierungsaufwand.

Ansatz Erkennt teure Einzelfelder Implementierungsaufwand Präzision
Field-Level Cost Analysis Ja, pro Feld Mittel bis hoch Sehr hoch
Depth-Limit Nein Gering Niedrig
Node-Count-Limit Teilweise Gering Mittel
HTTP-Rate-Limiting Nein Gering Niedrig

Der pragmatische Weg kombiniert meist mehrere Schichten: HTTP-Rate-Limiting als grobe erste Verteidigungslinie, ein moderates Depth-Limit gegen offensichtlich rekursive Schemas, und Field-Level Cost Analysis als präzise letzte Instanz für einzelne teure Felder. Keine der Techniken ersetzt die anderen vollständig, sie decken unterschiedliche Angriffs- und Fehlerklassen ab.

Mironsoft

GraphQL-Performance, API-Sicherheit und Query-Governance

Teure Felder bringen eure Datenbank ins Schwitzen?

Wir analysieren euer GraphQL-Schema, identifizieren kritische Felder und implementieren Field-Level Cost Analysis mit sinnvollen Budgets pro Client, ohne legitime Queries unnötig zu blockieren.

Schema-Audit

Kritische Felder mit teuren Joins und externen Calls identifizieren

Cost-Direktiven

Realistische Kostenwerte samt Multiplikatoren für Listen und Pagination

Monitoring

Dashboards, die geschätzte Kosten mit realer Latenz abgleichen

10. Zusammenfassung

Field-Level Cost Analysis löst ein Problem, das pauschale Tiefen- oder Knotenlimits systematisch übersehen: einzelne, unscheinbar wirkende Felder mit sehr hohen tatsächlichen Kosten. Kosten-Direktiven direkt im Schema machen diese Kosten für das gesamte Team sichtbar, dynamische Multiplikatoren erfassen Listen und verschachtelte Pagination korrekt, und clientabhängige Budgets erlauben unterschiedliche Limits für interne und öffentliche Nutzer.

Der entscheidende Vorteil gegenüber reaktiven Maßnahmen ist der Zeitpunkt der Prüfung: Die Kostenberechnung läuft vor der Ausführung, sodass teure Queries abgelehnt werden, bevor sie Datenbank oder externe Services belasten. In Kombination mit Monitoring, das geschätzte Kosten mit realer Latenz abgleicht, wird Field-Level Cost Analysis zu einem lernenden System, das mit der API mitwächst.

Field-Level Cost Analysis — Das Wichtigste auf einen Blick

Kosten-Direktiven

@cost direkt am Feld im SDL macht Kosten für das gesamte Team sichtbar und pflegbar.

Multiplikatoren

Listen und Pagination brauchen dynamische Kostenwerte statt statischer Pauschalen.

Client-Budgets

Interne Tools und öffentliche APIs erhalten unterschiedliche Kostenbudgets.

Prävention vor Reaktion

Ablehnung passiert vor der Ausführung, nicht erst nach überlasteter Datenbank.

11. FAQ: Field-Level Cost Analysis

1Unterschied zu Query-Complexity?
Query-Complexity zählt meist nur Felder pauschal. Field-Level Cost Analysis vergibt individuelle, realistische Kostenwerte inklusive Argumenten.
2Welche Bibliotheken eignen sich?
graphql-cost-analysis und graphql-query-complexity, beide als Validierungsregel in Apollo Server oder GraphQL Yoga einhängbar.
3Kosten bei Pagination berechnen?
Über Multiplikatoren, die Basiswert mit Argumentwert wie limit multiplizieren, bei Verschachtelung über alle Ebenen propagiert.
4Gleiches Budget für alle Clients?
Nein, interne oder vertrauenswürdige Clients können höhere Budgets erhalten, gelesen aus dem Auth-Kontext.
5Wie hoch sollte ein neuer Kostenwert sein?
Faustregel: Spaltenzugriff 1, Join oder externer Call 5 bis 20, große Aggregationen ab 50, geprüft im Code-Review.
6Vor oder nach Ausführung?
Vor der Ausführung, direkt nach Parsen und Validieren, sodass kein Resolver bei Überschreitung aufgerufen wird.
7Falsch geschätzten Kostenwert erkennen?
Durch Monitoring, das geschätzte Kosten mit realer Latenz vergleicht, bei Abweichung Direktive nachjustieren.
8Ersetzt es Rate-Limiting?
Nein, beide ergänzen sich. Rate-Limiting begrenzt Requests insgesamt, Cost Analysis einzelne teure Queries.
9Funktioniert es mit Federation?
Ja, aber die Berechnung muss zentral am Gateway erfolgen oder Kosten aus Subgraphen aggregieren.
10Wie viel Aufwand ist die Einführung?
Vor allem Bewerten und Annotieren bestehender Felder. Die technische Integration ist meist in wenigen Stunden erledigt.