Under-Fetching — REST vs GraphQL
In this tutorial, you'll learn about Under-Fetching. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
Under-fetching occurs when a single API call does not return all the data a client needs, forcing multiple round trips to assemble the complete data set.
What You'll Learn
By the end of this lesson, you will identify under-fetching scenarios, measure its performance impact, and understand how Graphql eliminates it with declarative nested queries.
Why It Matters
Under-fetching multiplies latency. If each call takes 100ms, three under-fetched calls take 300ms plus overhead, making apps feel slow even with fast individual endpoints.
Real-World Use
A dashboard showing user info, recent orders, and notifications requires 3+ REST calls: /user, /user/orders, /user/notifications. GraphQL fetches all in one query.
Under-Fetching Flow
sequenceDiagram
Client->>REST: GET /user/1
REST->>Client: {id, name, email}
Client->>REST: GET /user/1/orders
REST->>Client: [{id, total, status}]
Client->>REST: GET /notifications?user_id=1
REST->>Client: [{id, message}]
Note over Client: 3 round trips!
Client->>GQL: POST /graphql
Client->>GQL: {user(id:1){name orders{total} notifications{message}}}
GQL->>Client: {user:{name, orders:[...], notifications:[...]}}
Note over Client: 1 round trip!
Under-Fetching Latency Simulator
# underfetch_latency.py
import time
from typing import Any, Callable, Dict, List
class LatencySimulator:
def __init__(self, base_latency_ms: float = 100):
self.base_latency = base_latency_ms / 1000
def rest_call(self, endpoint: str) -> Dict:
time.sleep(self.base_latency)
data = {
"/user": {"id": 1, "name": "Alice", "email": "a@x.com"},
"/orders": [{"id": 101, "total": 50, "status": "delivered"}],
"/notifications": [{"id": 1, "message": "Welcome"}],
}
return data.get(endpoint, {})
def rest_assemble_profile(self) -> Dict:
start = time.time()
user = self.rest_call("/user")
orders = self.rest_call("/orders")
notifs = self.rest_call("/notifications")
elapsed = time.time() - start
return {
"calls": 3,
"elapsed_ms": round(elapsed * 1000),
"data": {**user, "orders": orders, "notifications": notifs},
}
def graphql_query(self) -> Dict:
start = time.time()
time.sleep(self.base_latency)
elapsed = time.time() - start
return {
"calls": 1,
"elapsed_ms": round(elapsed * 1000),
"data": {
"user": {"id": 1, "name": "Alice"},
"orders": [{"id": 101, "total": 50}],
"notifications": [{"id": 1, "message": "Welcome"}],
},
}
sim = LatencySimulator(base_latency_ms=100)
rest_result = sim.rest_assemble_profile()
gql_result = sim.graphql_query()
print(f"REST: {rest_result['calls']} calls, {rest_result['elapsed_ms']}ms")
print(f"GraphQL: {gql_result['calls']} call, {gql_result['elapsed_ms']}ms")
print(f"Speedup: {rest_result['elapsed_ms'] / gql_result['elapsed_ms']:.0f}x")
Expected output:
REST: 3 calls, 300ms
GraphQL: 1 call, 100ms
Speedup: 3x
N+1 Under-Fetching Pattern
# n_plus_one_underfetch.py
from typing import Any, Dict, List
class BlogData:
def get_posts(self) -> List[Dict]:
return [{"id": p, "title": f"Post {p}"} for p in range(1, 6)]
def get_author(self, post_id: int) -> Dict:
return {"id": post_id, "name": f"Author {post_id}"}
def rest_assemble_feed(self) -> Dict:
posts = self.get_posts()
# Under-fetching: no author data in posts
authors = [self.get_author(p["id"]) for p in posts]
return {
"calls": 1 + len(posts), # 1 for posts + N for authors
"feed": [
{**p, "author": a}
for p, a in zip(posts, authors)
],
}
def graphql_feed(self) -> Dict:
posts = self.get_posts()
authors = {p["id"]: {"name": f"Author {p['id']}"} for p in posts}
return {
"calls": 1,
"feed": [
{**p, "author": authors[p["id"]]}
for p in posts
],
}
blog = BlogData()
rest = blog.rest_assemble_feed()
gql = blog.graphql_feed()
print(f"REST calls to assemble feed: {rest['calls']}")
print(f"GraphQL calls to assemble feed: {gql['calls']}")
print(f"REST calls for {len(rest['feed'])} posts: {rest['calls']}")
Expected output:
REST calls to assemble feed: 6
GraphQL calls to assemble feed: 1
REST calls for 5 posts: 6
Partial Data Problem
# partial_data.py
from typing import Dict, List
class UserProfile:
def rest_profile(self, user_id: int) -> Dict:
user = {"id": user_id, "name": "Alice"}
# User endpoint does not return orders or friends
# Client must make additional calls
return user
def rest_orders(self, user_id: int) -> List[Dict]:
return [{"id": 101, "total": 50}]
def rest_friends(self, user_id: int) -> List[Dict]:
return [{"id": 2, "name": "Bob"}]
def rest_full_profile(self, user_id: int) -> Dict:
user = self.rest_profile(user_id)
orders = self.rest_orders(user_id)
friends = self.rest_friends(user_id)
return {
"calls": 3,
"data": {**user, "orders": orders, "friends": friends},
}
def graphql_full_profile(self, user_id: int) -> Dict:
return {
"calls": 1,
"data": {
"id": user_id,
"name": "Alice",
"orders": [{"id": 101, "total": 50}],
"friends": [{"id": 2, "name": "Bob"}],
},
}
profile = UserProfile()
rest = profile.rest_full_profile(1)
gql = profile.graphql_full_profile(1)
print(f"REST: {rest['calls']} separate calls to assemble profile")
print(f"GQL: {gql['calls']} single call with nested data")
Expected output:
REST: 3 separate calls to assemble profile
GQL: 1 single call with nested data
Common Mistakes
1. Not Recognizing Under-Fetching
A single screen making 5+ API calls is normal in REST. Each represents an under-fetched relationship.
2. Over-Aggregating REST Endpoints
Creating /user-with-everything solves under-fetching but causes over-fetching. GraphQL balances both.
3. Ignoring Sequential Call Chains
Waiting for one call to complete before making the next (Waterfall). Parallelize independent calls.
4. Not Using REST Batching
Tools like GraphQL or REST batch endpoints can reduce under-fetching. Facebook's original REST batch inspired GraphQL.
5. Under-Fetching in Lists
A list page that needs user names fetches each user individually. REST APIs often lack ?include for nested data.
Practice Questions
1. What is under-fetching in REST?
When one endpoint does not return all needed data, requiring additional calls to assemble the complete result.
2. How many REST calls might a dashboard need?
3-10 calls depending on the number of data sources the dashboard displays.
3. How does GraphQL prevent under-fetching?
A single query can include multiple related resources with nested field selection.
4. What is the N+1 under-fetching pattern?
One call to get a list of N items, then N calls to get details for each item.
Challenge
Build a REST API that uses a batch endpoint to solve under-fetching, accepting a list of resources to return in a single response.
FAQ
Mini Project: Under-Fetching Detector
# underfetch_detector.py
from typing import Dict, List
class UnderfetchDetector:
def __init__(self):
self.screens = {}
def register_screen(self, name: str, api_calls: int):
self.screens[name] = api_calls
def report(self):
print(f"{'Screen':20s} {'Calls':6s} {'Status':10s}")
print("-" * 36)
for name, calls in self.screens.items():
status = "OK" if calls <= 2 else "UNDER-FETCH" if calls <= 5 else "SEVERE"
print(f"{name:20s} {calls:6d} {status:10s}")
detector = UnderfetchDetector()
detector.register_screen("Profile", 5)
detector.register_screen("Dashboard", 8)
detector.register_screen("Login", 1)
detector.report()
Expected output:
Screen Calls Status
------------------------------------
Profile 5 UNDER-FETCH
Dashboard 8 SEVERE
Login 1 OK
What's Next
You understand under-fetching. Next, explore round trip analysis, then compare REST caching.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro