Round Trips — REST vs GraphQL Comparison
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
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