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.
Inhaltsverzeichnis
- 1. Warum viele Requests bei GraphQL überflüssig sind
- 2. Wie ein gebündelter HTTP-Request aussieht
- 3. Batching auf Client-Seite mit Apollo Link Batch
- 4. Batching serverseitig implementieren
- 5. Batching vs. DataLoader: zwei verschiedene Ebenen
- 6. HTTP/2-Multiplexing als Alternative zu Batching
- 7. Fehlerbehandlung bei gebündelten Requests
- 8. Wo Batching an seine Grenzen stößt
- 9. Batching-Strategien im Vergleich
- 10. Zusammenfassung
- 11. FAQ
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.