GraphQL: Queries, Performance and Caching
2.3 Query Model
GraphQL gives clients full control over what data they receive. The client writes the query, and the server returns exactly that shape.
Nested Queries
Queries can go deep into related data:
{
user(id: 1) {
posts {
comments {
author {
name
}
}
}
}
}
This is powerful but dangerous without limits. A malicious client can nest 50 levels deep and crash the server.
Fragments
Fragments are reusable sets of fields. Define once, use in many queries:
fragment UserFields on User {
id
name
email
}
query {
user(id: 1) { ...UserFields }
admin(id: 2) { ...UserFields }
}
Fragments reduce duplication and make queries easier to maintain.
Aliases
Aliases rename fields in the response. Useful when querying the same field with different arguments:
{
admins: users(role: ADMIN) { name }
regular: users(role: USER) { name }
}
Response: { "admins": [...], "regular": [...] } — clear separation in one request.
Variables
Variables parameterize queries. They prevent string interpolation (which is unsafe):
query GetUser($id: ID!) {
user(id: $id) {
name
email
}
}
Variables are sent as a separate JSON object: { "id": "user-123" }. The server validates variable types against the schema.
Built-in Directives
Every spec-compliant GraphQL server must support these three:
| Directive | Where | Behavior |
|---|---|---|
@skip(if: Boolean!) |
Query | Exclude field when if is true |
@include(if: Boolean!) |
Query | Include field only when if is true |
@deprecated(reason: String) |
Schema | Mark field as deprecated, signal clients to migrate |
Example: user(id: 1) { name email @skip(if: $hideEmail) }
Query Validation
The server validates every query before execution:
| Check | Example | Result |
|---|---|---|
| Unknown field | user { foo } |
Validation error |
| Wrong argument type | user(id: 123) when ID expected |
Validation error |
| Missing required arg | user { name } (no id) |
Validation error |
| Syntax error | { user( } |
Parse error |
All errors are caught before any resolver runs. This saves server resources.
2.4 Performance
Query Depth Limiting
Set a maximum depth (e.g., 10 levels). Reject queries that go deeper:
Allowed: user -> posts -> comments (depth 3) ✓
Rejected: user -> posts -> comments -> ... (depth 15) ✗
Depth limiting is the first line of defense against abuse.
Query Complexity Analysis
Assign a cost to each field. Sum all costs. Reject if total exceeds limit:
| Field Type | Cost Example |
|---|---|
| Scalar field | 1 |
| Object field | 5 |
| List field | 10 |
| Connection | 20 |
A query requesting 100 list fields would cost 1000. If the limit is 500, the server rejects it with the complexity score in the error message.
N+1 Problem
This is the most common performance issue in GraphQL:
- Without DataLoader: 1 query for parent + N queries for children = N+1 total.
- With DataLoader: 1 query for parent + 1 batched query for all children = 2 total.
Always use DataLoader. There is no valid reason to skip it in production.
Resolver Waterfall
Resolvers run in sequence: parent first, then children. If a resolver is slow, all child resolvers wait:
user (200ms) -> posts (150ms) -> comments (100ms) = 450ms total
Solutions: optimize slow resolvers, use DataLoader for batching, add caching at resolver level.
2.5 Transport
HTTP
Most common transport. All operations use POST with query in request body:
POST /graphql
Content-Type: application/json
{ "query": "{ user(id: 1) { name } }", "variables": {} }
For persisted queries, GET is possible: /graphql?extensions={"persistedQuery":{"sha256Hash":"abc123"}}.
WebSocket
Used for subscriptions. Two protocols exist:
graphql-ws— modern, recommended.subscriptions-transport-ws— legacy, deprecated.
Flow: client connects -> sends subscribe message -> server pushes events -> client unsubscribes -> disconnect.
2.6 Caching
Client-Side Caching
Apollo Client normalizes data by __typename + id. When a mutation updates User:1, all queries showing that user re-render automatically. No manual cache updates needed.
Persisted Queries
Client sends a SHA-256 hash instead of the full query string. Server maps hash → query. Benefits: smaller requests, blocks arbitrary queries, enables CDN GET caching.
Server-Side Caching
| Approach | How It Works | Limitation |
|---|---|---|
| Response caching | Cache by query hash | Low hit rate |
| Field-level caching | Cache resolver results | Complex invalidation |
| CDN caching | Cache persisted GET responses | Persisted queries only |
| DataLoader | Cache within one request | No cross-request benefit |
GraphQL caching is harder than REST. Plan your strategy early.