Skip to content

Round Trips — REST vs GraphQL Comparison

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you'll learn about Round Trips. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.

Round trips are the number of HTTP requests a client must make to fetch all data for a single screen. REST often requires multiple round trips while GraphQL typically needs one.

What You'll Learn

By the end of this lesson, you will analyze round trip costs, understand waterfall vs parallel requests, and know how to minimize round trips in both REST and GraphQL.

Why It Matters

Each round trip adds TCP handshake, TLS negotiation, DNS resolution, and HTTP overhead. On mobile networks, each round trip can take 200-500ms before any data is transferred.

Real-World Use

Instagram's REST API required 5+ round trips to load the feed. Their GraphQL Migration reduced it to 1-2 round trips, significantly improving perceived performance on mobile.

Round Trip Anatomy

sequenceDiagram
    Client->>Server: TCP SYN
    Server->>Client: SYN-ACK
    Client->>Server: ACK + HTTP Request
    Server->>Client: Response
    Note over Client,Server: 1 round trip = ~100ms (wired)
    Note over Client,Server: 1 round trip = ~300ms (mobile 4G)
    Note over Client,Server: 1 round trip = ~1000ms (mobile 3G)

Round Trip Cost Calculator

# roundtrip_cost.py
from typing import Dict, List

class RoundTripCalculator:
    def __init__(self, base_latency_ms: float = 100):
        self.base_latency = base_latency_ms

    def cost(self, num_trips: int, parallel: bool = False) -> Dict:
        if parallel:
            total_ms = self.base_latency
        else:
            total_ms = num_trips * self.base_latency

        return {
            "trips": num_trips,
            "parallel": parallel,
            "total_ms": total_ms,
            "bandwidth_overhead_kb": num_trips * 0.5,  # ~500 bytes per HTTP overhead
        }

    def compare_strategies(self, rest_trips: int, gql_trips: int = 1) -> Dict:
        rest_sequential = self.cost(rest_trips, parallel=False)
        rest_parallel = self.cost(rest_trips, parallel=True)
        gql = self.cost(gql_trips, parallel=False)

        return {
            "rest_sequential_ms": rest_sequential["total_ms"],
            "rest_parallel_ms": rest_parallel["total_ms"],
            "graphql_ms": gql["total_ms"],
            "rest_bandwidth_kb": rest_sequential["bandwidth_overhead_kb"],
            "graphql_bandwidth_kb": gql["bandwidth_overhead_kb"],
        }

calc = RoundTripCalculator(base_latency_ms=100)
comparison = calc.compare_strategies(rest_trips=5)

print(f"REST sequential (5 calls): {comparison['rest_sequential_ms']}ms")
print(f"REST parallel    (5 calls): {comparison['rest_parallel_ms']}ms")
print(f"GraphQL          (1 call):  {comparison['graphql_ms']}ms")
print(f"REST HTTP overhead:  {comparison['rest_bandwidth_kb']}KB")
print(f"GraphQL HTTP overhead: {comparison['graphql_bandwidth_kb']}KB")

Expected output:

REST sequential (5 calls): 500ms
REST parallel    (5 calls): 100ms
GraphQL          (1 call):  100ms
REST HTTP overhead:  2.5KB
GraphQL HTTP overhead: 0.5KB

Waterfall vs Parallel

# waterfall_vs_parallel.py
from typing import Dict, List

class RequestScheduler:
    def __init__(self):
        self.data_store = {
            "user": {"id": 1, "name": "Alice"},
            "orders": [{"id": 101, "total": 50}],
            "products": [{"id": 1, "name": "Widget"}],
            "notifications": [{"id": 1, "text": "Hello"}],
            "settings": {"theme": "dark"},
        }

    def rest_waterfall(self) -> Dict:
        data = {}
        data["user"] = self.data_store["user"]
        # Must wait for user to know orders
        data["orders"] = self.data_store["orders"]
        # Must wait for orders to know products
        product_ids = [101]
        data["products"] = [
            p for p in self.data_store["products"] if p["id"] in product_ids
        ]
        return {"calls": 3, "style": "waterfall", "data": data}

    def rest_parallel(self) -> Dict:
        # Fetch all independent resources at once
        return {
            "calls": 4,
            "style": "parallel",
            "data": {**self.data_store},
        }

    def graphql(self) -> Dict:
        return {
            "calls": 1,
            "style": "single",
            "data": {**self.data_store},
        }

scheduler = RequestScheduler()
print(f"Waterfall: {scheduler.rest_waterfall()['calls']} calls (serialized)")
print(f"Parallel:  {scheduler.rest_parallel()['calls']} calls (simultaneous)")
print(f"GraphQL:   {scheduler.graphql()['calls']} call")

Expected output:

Waterfall: 3 calls (serialized)
Parallel:  4 calls (simultaneous)
GraphQL:   1 call

Round Trip Scaling

# roundtrip_scaling.py
from typing import Dict, List

class RoundTripScaling:
    @staticmethod
    def rest_time_for_posts(n_posts: int, latency_ms: float = 100) -> Dict:
        posts_call = latency_ms
        author_calls = n_posts * latency_ms
        comment_calls = n_posts * latency_ms
        total = posts_call + author_calls + comment_calls

        return {
            "posts_call_ms": posts_call,
            "author_calls_ms": author_calls,
            "comment_calls_ms": comment_calls,
            "total_ms": total,
            "total_calls": 1 + n_posts + n_posts,
        }

    @staticmethod
    def graphql_time(latency_ms: float = 100) -> Dict:
        return {
            "total_ms": latency_ms,
            "total_calls": 1,
        }

scale = RoundTripScaling()
for n in [5, 10, 50]:
    rest = scale.rest_time_for_posts(n)
    gql = scale.graphql_time()
    print(f"{n} posts: REST={rest['total_ms']}ms ({rest['total_calls']} calls) vs GQL={gql['total_ms']}ms")

Expected output:

5 posts: REST=1100ms (11 calls) vs GQL=100ms
10 posts: REST=2100ms (21 calls) vs GQL=100ms
50 posts: REST=10100ms (101 calls) vs GQL=100ms

Common Mistakes

1. Ignoring Mobile Network Latency

Testing on localhost hides round trip costs. Always test on actual mobile networks or throttled connections.

2. Not Using HTTP/2 Multiplexing

HTTP/2 allows multiplexed streams over one connection, reducing connection overhead for REST parallel calls.

3. Sequential Calls When Parallel Works

Making dependent calls sequentially. Use Promise.all or async patterns for independent resources.

4. Over-Estimating GraphQL Round Trip Savings

If the server must internally make multiple resolver calls, the GraphQL response time may equal REST parallel time.

5. Not Caching at Any Level

Without caching, every round trip hits the server. Cache at CDN, HTTP, and resolver levels.

Practice Questions

1. How many round trips does typical REST need for a profile page?

3-5 calls (user, orders, notifications, friends, settings).

2. Why are round trips expensive on mobile?

TCP handshake, TLS negotiation, and radio wake-up add 200-500ms per trip beyond data transfer time.

3. Can parallel REST calls match GraphQL speed?

Yes, if all calls are independent and the network supports parallel requests. GraphQL has overhead advantage.

4. What HTTP feature reduces round trip overhead?

HTTP/2 multiplexing allows multiple requests over one connection.

Challenge

Build a round trip profiler that captures real API call patterns from a browser or app and reports total round trip time for REST vs GraphQL equivalents.

FAQ

How many round trips is acceptable?

For mobile, aim for 1-2 round trips per screen. For web, 3-5 is acceptable with HTTP/2.

Does bundling REST calls help?

Bundling (batch endpoint) reduces round trips but introduces coupling. GraphQL is the standard solution.

What is a waterfall request?

Request B depends on data from Request A. B cannot start until A completes. Common in REST.

How does CDN caching affect round trips?

CDN caches reduce server round trips for cached responses, but the client still makes the HTTP request.

Does GraphQL eliminate all round trips?

For a single query, yes. But mutations may require separate calls. Some data may need multiple queries.

Mini Project: Round Trip Optimizer

# roundtrip_optimizer.py
from typing import Dict, List

class RoundTripOptimizer:
    @staticmethod
    def optimize(endpoints: List[str], depends_on: Dict[str, str]) -> Dict:
        rounds = []
        remaining = set(endpoints)
        while remaining:
            batch = set()
            for ep in remaining:
                dep = depends_on.get(ep)
                if dep is None or dep not in remaining:
                    batch.add(ep)
            rounds.append(list(batch))
            remaining -= batch
        return {
            "original_calls": len(endpoints),
            "optimized_rounds": len(rounds),
            "rounds": rounds,
        }

opt = RoundTripOptimizer()
result = opt.optimize(
    endpoints=["user", "orders", "products", "notifications"],
    depends_on={"orders": "user", "products": "orders"},
)
print(f"Original: {result['original_calls']} calls")
print(f"Optimized: {result['optimized_rounds']} rounds")
for i, batch in enumerate(result['rounds']):
    print(f"  Round {i+1}: {batch}")

Expected output:

Original: 4 calls
Optimized: 3 rounds
  Round 1: ['notifications', 'user']
  Round 2: ['orders']
  Round 3: ['products']

What's Next

You understand round trips. Next, explore REST caching, then GraphQL caching.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro