Skip to content

Under-Fetching — REST vs GraphQL

DodaTech Updated 2026-06-28 5 min read

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

Is under-fetching worse than over-fetching?

Under-fetching usually hurts more because of network latency. Over-fetching wastes bandwidth. Both hurt mobile the most.

Can REST solve under-fetching?

Yes, with compound endpoints (/user-with-profile) or ?include=orders,friends. But these solutions are ad-hoc.

Does under-fetching affect server performance?

More calls mean more connection overhead, more authentication checks, and more load.

How do you measure under-fetching?

Count the number of API calls needed to render one screen. More than 2-3 suggests under-fetching.

What is the relationship between under-fetching and N+1?

N+1 is a specific form of under-fetching where you fetch a list then each item individually.

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