GraphQL Batching: mehrere Queries in einem HTTP-Request bündeln
AI generated
{ }
type
GraphQL · Batching · Performance · HTTP
GraphQL Batching
mehrere Queries, ein einziger HTTP-Roundtrip

Ein Dashboard, das gleichzeitig Nutzerdaten, Bestellhistorie und Produktempfehlungen lädt, feuert ohne Batching drei separate HTTP-Requests mit jeweils eigenem TLS-Handshake und eigener Latenz ab. GraphQL Batching bündelt solche unabhängigen Operationen in einem einzigen HTTP-Request und reduziert damit die Anzahl der Roundtrips, ohne das Schema oder die Resolver-Logik zu verändern.

17 Min. Lesezeit Apollo Link Batch · DataLoader · HTTP/2 Apollo Client · Apollo Server · Magento GraphQL

1. Warum viele Requests bei GraphQL überflüssig sind

GraphQL erlaubt es bereits von Haus aus, mehrere Felder in einer einzigen Query zu kombinieren, doch in der Praxis entstehen trotzdem oft mehrere getrennte HTTP-Requests. Das passiert typischerweise, wenn unabhängige UI-Komponenten jeweils ihre eigene Query auslösen, etwa ein Warenkorb-Widget, eine Produktempfehlung und ein Nutzerprofil-Badge, die alle gleichzeitig beim Seitenaufbau laden. Ohne GraphQL Batching feuert der Client für jede dieser Komponenten einen eigenen HTTP-Request ab, jeweils mit eigenem TLS-Handshake, eigenen Headern und eigener Netzwerk-Latenz.

Der Overhead summiert sich besonders bei mobilen Verbindungen mit hoher Latenz: Selbst wenn jede einzelne Query serverseitig in wenigen Millisekunden beantwortet wird, addiert sich die Round-Trip-Time jedes einzelnen Requests, statt parallel oder gebündelt zu laufen. GraphQL Batching löst dieses Problem, indem mehrere unabhängige Operationen zu einem einzigen HTTP-Request zusammengefasst werden, der Server sie gemeinsam verarbeitet und eine gebündelte Antwort zurückschickt.

2. Wie ein gebündelter HTTP-Request aussieht

Technisch ist GraphQL Batching denkbar einfach: Statt eines einzelnen JSON-Objekts im Request-Body mit den Feldern query, variables und operationName schickt der Client ein JSON-Array mit mehreren solcher Objekte. Der Server erkennt am Array-Format, dass es sich um einen Batch handelt, verarbeitet jede Operation einzeln und antwortet ebenfalls mit einem Array, dessen Reihenfolge exakt der Reihenfolge der Anfragen entspricht. Diese Konvention ist nicht Teil der offiziellen GraphQL-Spezifikation, hat sich aber durch Apollo als De-facto-Standard etabliert.

Wichtig für die Implementierung von GraphQL Batching: Jede Operation im Batch wird unabhängig validiert und ausgeführt, ein Fehler in einer Operation beeinflusst nicht die anderen. Der Server muss also weiterhin für jede Operation im Array einzeln prüfen, ob sie autorisiert und syntaktisch korrekt ist, und darf nicht den gesamten Batch verwerfen, nur weil eine einzelne Operation fehlschlägt.


[
  {
    "query": "query GetCart($cartId: ID!) { cart(id: $cartId) { items { id name price } } }",
    "variables": { "cartId": "abc123" }
  },
  {
    "query": "query GetRecommendations($productId: ID!) { recommendations(productId: $productId) { id name } }",
    "variables": { "productId": "xyz789" }
  },
  {
    "query": "query GetProfile { me { firstName loyaltyPoints } }"
  }
]

3. Batching auf Client-Seite mit Apollo Link Batch

Apollo Client bietet mit BatchHttpLink eine eingebaute Lösung für GraphQL Batching, die transparent im Hintergrund arbeitet. Statt jeden useQuery-Aufruf sofort als eigenen HTTP-Request abzuschicken, sammelt BatchHttpLink alle Anfragen, die innerhalb eines konfigurierbaren Zeitfensters, meist zehn Millisekunden, ausgelöst werden, und schickt sie gebündelt als ein einziges Array. Aus Sicht der React-Komponenten ändert sich nichts: useQuery und useMutation funktionieren unverändert, das Batching passiert vollständig transparent auf der Transport-Ebene.

Diese Transparenz ist einer der größten Vorteile von GraphQL Batching gegenüber manuellem Zusammenfassen von Queries im Frontend-Code: Entwicklerteams müssen ihre Komponentenstruktur nicht anpassen oder Queries künstlich zusammenführen, nur um Netzwerk-Overhead zu sparen. Jede Komponente behält ihre eigene, fokussierte Query, während die Batching-Logik auf Transport-Ebene automatisch für die Bündelung sorgt.


// apollo-client.ts — batching multiple independent queries transparently
import { ApolloClient, InMemoryCache } from '@apollo/client';
import { BatchHttpLink } from '@apollo/client/link/batch-http';

const batchLink = new BatchHttpLink({
  uri: 'https://api.mironsoft.de/graphql',
  batchMax: 10,        // batch up to 10 operations per request
  batchInterval: 20,   // wait up to 20ms to collect operations
});

export const client = new ApolloClient({
  link: batchLink,
  cache: new InMemoryCache(),
});

// Component code stays unchanged — batching is fully transparent
function Dashboard() {
  const { data: cart } = useQuery(GET_CART, { variables: { cartId } });
  const { data: recs } = useQuery(GET_RECOMMENDATIONS, { variables: { productId } });
  const { data: profile } = useQuery(GET_PROFILE);
  // All three queries are combined into a single HTTP request automatically
}

4. Batching serverseitig implementieren

Auf Serverseite muss GraphQL Batching explizit unterstützt werden, da ein normaler GraphQL-Server standardmäßig nur ein einzelnes Operation-Objekt im Request-Body erwartet. Apollo Server erkennt Array-Bodies automatisch und verarbeitet jede Operation im Batch parallel, sofern kein expliziter Grund für sequenzielle Verarbeitung besteht. Wichtig ist, dass jede Operation weiterhin ihren eigenen GraphQL-Context bekommt, damit etwa Autorisierungsprüfungen pro Operation korrekt und unabhängig voneinander laufen.

Für selbstgebaute GraphQL-Server oder Frameworks ohne eingebaute Batching-Unterstützung lässt sich die Logik meist mit wenigen Zeilen Middleware nachrüsten: Der Request-Handler prüft, ob der Body ein Array ist, und iteriert bei Bedarf über die Einzeloperationen, statt eine einzelne Operation vorauszusetzen. Die Herausforderung liegt weniger im Parsen des Arrays als in der korrekten Fehlerisolierung zwischen den Operationen im selben GraphQL Batching-Request.


// batch-middleware.js — minimal batching support for a custom GraphQL server
async function handleGraphQLRequest(req, res) {
  const body = req.body;
  const isBatch = Array.isArray(body);
  const operations = isBatch ? body : [body];

  const results = await Promise.all(
    operations.map(async (op) => {
      try {
        // Each operation gets its own fresh context (auth, dataloaders, etc.)
        const context = await createContext(req);
        return await executeGraphQL(schema, op.query, op.variables, context);
      } catch (error) {
        // Isolate failures — one bad operation must not break the whole batch
        return { errors: [{ message: error.message }] };
      }
    })
  );

  res.json(isBatch ? results : results[0]);
}

5. Batching vs. DataLoader: zwei verschiedene Ebenen

Ein häufiges Missverständnis ist, GraphQL Batching mit DataLoader-Batching zu verwechseln, obwohl beide unterschiedliche Probleme lösen. HTTP-Batching, wie in diesem Artikel beschrieben, bündelt mehrere komplette, unabhängige GraphQL-Operationen in einem Request, um Netzwerk-Overhead zu sparen. DataLoader-Batching dagegen löst das N+1-Problem innerhalb einer einzelnen Query: Wenn ein Resolver für jedes Element einer Liste einzeln eine Datenbankabfrage auslösen würde, sammelt DataLoader diese Einzelabfragen innerhalb eines Event-Loop-Ticks und führt sie als eine gebündelte Datenbankabfrage aus.

Beide Techniken ergänzen sich, lösen aber unterschiedliche Engpässe: HTTP-Batching reduziert die Anzahl der Netzwerk-Roundtrips zwischen Client und Server, DataLoader-Batching reduziert die Anzahl der Datenbankabfragen innerhalb eines einzelnen Requests. Ein produktives Setup mit GraphQL Batching auf Transport-Ebene profitiert zusätzlich fast immer von DataLoader auf Resolver-Ebene, da beide Optimierungen an unterschiedlichen Stellen der Anfrage-Pipeline ansetzen.


// dataloader.js — batches per-request DB lookups, separate from HTTP batching
import DataLoader from 'dataloader';

function createProductLoader(db) {
  return new DataLoader(async (productIds) => {
    // A single query for all IDs collected within this event loop tick
    const rows = await db.query(
      'SELECT * FROM products WHERE id = ANY($1)',
      [productIds]
    );
    const byId = new Map(rows.map((row) => [row.id, row]));
    return productIds.map((id) => byId.get(id) ?? null);
  });
}

// Resolver stays simple — DataLoader handles the batching transparently
const resolvers = {
  Product: {
    relatedProducts: (product, _args, { loaders }) =>
      loaders.product.loadMany(product.relatedProductIds),
  },
};

6. HTTP/2-Multiplexing als Alternative zu Batching

Mit HTTP/2 und dessen eingebautem Multiplexing stellt sich die Frage, ob GraphQL Batching überhaupt noch nötig ist, da mehrere parallele Requests über dieselbe TCP-Verbindung ohne zusätzlichen Handshake-Overhead laufen können. Tatsächlich reduziert HTTP/2 einen Großteil des klassischen Overhead-Problems von vielen einzelnen HTTP/1.1-Requests, insbesondere den wiederholten TLS-Handshake und die begrenzte Anzahl paralleler Verbindungen pro Domain.

Dennoch bleibt GraphQL Batching auch mit HTTP/2 in bestimmten Szenarien sinnvoll: Jede Operation im Batch teilt sich weiterhin denselben Response-Header-Overhead und dieselbe Verarbeitungszeit für Verbindungsmanagement auf Server-Seite, was bei sehr vielen kleinen Queries messbar ist. Zudem unterstützen nicht alle Infrastrukturkomponenten, etwa ältere Load Balancer oder CDNs, HTTP/2 durchgängig, sodass Batching eine protokollunabhängige Optimierung bleibt, die unabhängig von der HTTP-Version funktioniert.


# Sending a batched request manually with curl for testing/debugging
curl -X POST https://api.mironsoft.de/graphql \
  -H "Content-Type: application/json" \
  -d '[
    {"query": "query GetCart($id: ID!) { cart(id: $id) { items { name } } }", "variables": {"id": "abc123"}},
    {"query": "query GetProfile { me { firstName } }"}
  ]'

# Response is an array in the same order as the requests
# [
#   { "data": { "cart": { "items": [{ "name": "Sneaker" }] } } },
#   { "data": { "me": { "firstName": "Anna" } } }
# ]

7. Fehlerbehandlung bei gebündelten Requests

Ein wichtiger Aspekt von GraphQL Batching ist der Umgang mit Teilausfällen: Scheitert eine Operation im Batch, etwa durch einen Autorisierungsfehler oder einen internen Serverfehler, dürfen die anderen Operationen im selben Request davon nicht betroffen sein. Client-seitig muss die Antwort-Verarbeitung deshalb jede Position im Antwort-Array einzeln auswerten, statt den gesamten Batch bei einem einzelnen Fehler zu verwerfen. Apollo Client übernimmt diese Zuordnung automatisch und liefert jeder ursprünglichen useQuery-Instanz nur ihr eigenes Ergebnis samt möglicher Fehler.

Ein subtileres Problem entsteht, wenn eine einzelne Operation im Batch ungewöhnlich lange braucht, etwa eine teure Aggregation, während die anderen Operationen längst fertig wären. Da der HTTP-Response erst gesendet wird, wenn alle Operationen im Batch abgeschlossen sind, blockiert die langsamste Operation die schnellsten. Für Anwendungsfälle mit stark unterschiedlichen Antwortzeiten ist es deshalb sinnvoll, teure und schnelle Queries nicht im selben GraphQL Batching-Request zu kombinieren, sondern gezielt zu trennen.

8. Wo Batching an seine Grenzen stößt

GraphQL Batching ist keine universelle Lösung. Für zeitkritische Interaktionen wie eine Live-Suche mit Autovervollständigung kann das Warten auf das Batching-Zeitfenster, meist zehn bis zwanzig Millisekunden, spürbare Latenz hinzufügen, ohne einen echten Performance-Gewinn zu bringen, da hier ohnehin nur eine einzelne Query läuft. Für Subscriptions ist Batching grundsätzlich nicht anwendbar, da diese über eine dauerhafte WebSocket-Verbindung statt über einzelne HTTP-Requests laufen.

Auch beim Caching entstehen Komplikationen: HTTP-Caching auf Basis von GET-Requests und URL-basiertem Cache-Keying funktioniert bei gebündelten POST-Requests nicht, da jeder Batch potenziell eine andere Kombination von Operationen enthält. Wer stark auf HTTP-Level-Caching setzt, etwa über ein CDN vor der GraphQL-API, sollte GraphQL Batching gezielt nur für nicht-cachebare, dynamische Operationen einsetzen und cachebare Queries separat behandeln.

9. Batching-Strategien im Vergleich

Die folgende Tabelle vergleicht GraphQL Batching mit verwandten Optimierungsansätzen nach Wirkungsbereich und typischen Einsatzszenarien.

Technik Wirkungsbereich Löst Typischer Einsatz
HTTP-Batching Client-Server-Transport Viele parallele Requests Dashboards, viele unabhängige Widgets
DataLoader-Batching Resolver-Ebene N+1-Datenbankabfragen Listen mit verschachtelten Feldern
HTTP/2-Multiplexing Transport-Protokoll TCP-Verbindungs-Overhead Allgemein, protokollweit
Query-Zusammenlegung im Frontend Anwendungscode Redundante Queries Eng gekoppelte Komponenten

Die robusteste Konfiguration kombiniert GraphQL Batching auf Transport-Ebene mit DataLoader auf Resolver-Ebene, da beide unterschiedliche Engpässe adressieren und sich nicht gegenseitig ausschließen. Query-Zusammenlegung im Frontend-Code bleibt meist die letzte Option, da sie die Modularität der Komponenten beeinträchtigt.

Mironsoft

GraphQL-Performance, API-Architektur und Magento-Integrationen

Zu viele Netzwerk-Roundtrips in eurem GraphQL-Frontend?

Wir analysieren euer Request-Muster, richten Client- und Server-seitiges Batching ein und kombinieren es mit DataLoader für eine spürbar schnellere API.

Request-Analyse

Identifikation redundanter Requests und Batching-Potenzial im Frontend

Batching-Setup

Apollo Link Batch und serverseitige Batch-Unterstützung produktiv einrichten

Performance-Tuning

DataLoader-Integration und HTTP/2 gemeinsam mit Batching optimieren

10. Zusammenfassung

GraphQL Batching reduziert die Anzahl der HTTP-Roundtrips, indem mehrere unabhängige Queries und Mutations zu einem einzigen Request gebündelt werden, formatiert als JSON-Array statt einzelnem Operation-Objekt. Apollo Link Batch macht diese Optimierung auf Client-Seite transparent, ohne dass Komponenten ihre Queries künstlich zusammenlegen müssen, während der Server jede Operation im Batch unabhängig verarbeitet und Fehler sauber isoliert.

Wichtig ist, GraphQL Batching nicht mit DataLoader-Batching zu verwechseln, das ein anderes Problem, das N+1-Query-Problem innerhalb einer einzelnen Anfrage, löst. Beide Techniken ergänzen sich in einer produktiven Architektur. Batching stößt an Grenzen bei zeitkritischen Einzelqueries, bei Subscriptions und beim HTTP-Level-Caching, weshalb es gezielt für Szenarien mit vielen parallelen, unabhängigen Operationen eingesetzt werden sollte, statt es pauschal überall zu aktivieren.

GraphQL Batching — Das Wichtigste auf einen Blick

JSON-Array statt Einzelobjekt

Mehrere Operationen werden als Array im Request-Body gebündelt, der Server antwortet mit einem passenden Array.

Transparent auf Client-Seite

Apollo Link Batch bündelt Anfragen innerhalb eines Zeitfensters automatisch, Komponentencode bleibt unverändert.

Nicht mit DataLoader verwechseln

HTTP-Batching reduziert Roundtrips, DataLoader reduziert Datenbankabfragen. Beide Techniken ergänzen sich.

Grenzen kennen

Nicht für Subscriptions, zeitkritische Einzelqueries oder HTTP-Level-Caching geeignet.

11. FAQ: GraphQL Batching

1Was ist GraphQL Batching?
Bündelung mehrerer Operationen als JSON-Array in einem HTTP-Request, statt einzelner Requests pro Operation.
2Ist das Teil der offiziellen Spezifikation?
Nein, De-facto-Standard durch Apollo, aber von den meisten gängigen Servern unterstützt.
3Unterschied zu DataLoader?
HTTP-Batching spart Roundtrips, DataLoader spart Datenbankabfragen innerhalb einer Query.
4Muss ich Komponenten anpassen?
Nein, mit BatchHttpLink läuft die Optimierung vollständig transparent auf Transport-Ebene.
5Was passiert bei Teilfehlern?
Nur die betroffene Operation liefert einen Fehler, alle anderen Operationen laufen unabhängig weiter.
6Macht HTTP/2 Batching überflüssig?
Nicht vollständig, Batching bleibt protokollunabhängig sinnvoll und spart weiter Header-Overhead.
7Warum bricht HTTP-Caching?
POST-Requests mit wechselnder Operations-Kombination lassen sich nicht URL-basiert cachen.
8Bremst eine langsame Operation die anderen?
Ja, die Response wartet auf alle Operationen. Teure und schnelle Queries getrennt halten.
9Funktioniert das bei Subscriptions?
Nein, Subscriptions laufen über WebSockets, nicht über einzelne HTTP-Requests.
10Wie in Apollo Server aktivieren?
Array-Bodies werden automatisch erkannt, keine zusätzliche Konfiguration nötig.