Back to Blog
gRPC vs REST vs GraphQL: Performance Deep Dive with Benchmarks

gRPC vs REST vs GraphQL: Performance Deep Dive with Benchmarks

December 19, 2025
9 min read
Tushar Agrawal

Comprehensive performance comparison of gRPC, REST, and GraphQL. Real benchmarks, latency analysis, throughput testing, and when to use each protocol in production systems.

Introduction

Choosing the right API protocol can make or break your system's performance. I've benchmarked gRPC, REST, and GraphQL extensively while building microservices at scale, and the results often surprise developers.

This isn't another surface-level comparison. We'll dive into actual benchmarks, memory profiles, and production patterns that reveal the true performance characteristics of each protocol.

Protocol Architecture Overview

REST (HTTP/1.1 + JSON)

┌─────────────────────────────────────────────────────┐
│                    REST API                          │
├─────────────────────────────────────────────────────┤
│  Transport: HTTP/1.1 (or HTTP/2)                    │
│  Serialization: JSON (text-based)                   │
│  Schema: OpenAPI/Swagger (optional)                 │
│  Connection: Request-Response                        │
└─────────────────────────────────────────────────────┘

Client Server │ │ │──── GET /users/123 ───────────────>│ │ │ │<─── {"id":123,"name":"..."} ───────│ │ │

gRPC (HTTP/2 + Protocol Buffers)

┌─────────────────────────────────────────────────────┐
│                    gRPC                              │
├─────────────────────────────────────────────────────┤
│  Transport: HTTP/2 (multiplexed streams)            │
│  Serialization: Protocol Buffers (binary)           │
│  Schema: .proto files (required)                    │
│  Connection: Unary, Streaming, Bidirectional        │
└─────────────────────────────────────────────────────┘

Client Server │ │ │════ Stream 1: GetUser ════════════>│ │════ Stream 2: ListOrders ═════════>│ (Multiplexed) │<═══ Binary Response ═══════════════│ │<═══ Binary Response ═══════════════│ │ │

GraphQL (HTTP + JSON with Query Language)

┌─────────────────────────────────────────────────────┐
│                   GraphQL                            │
├─────────────────────────────────────────────────────┤
│  Transport: HTTP/1.1 or HTTP/2                      │
│  Serialization: JSON                                 │
│  Schema: SDL (required)                              │
│  Connection: Single endpoint, flexible queries      │
└─────────────────────────────────────────────────────┘

Client Server │ │ │──── POST /graphql ────────────────>│ │ query { user(id:123) { │ │ name, orders { total } │ │ }} │ │<─── {"data":{...}} ────────────────│ │ │

Benchmark Setup

I ran benchmarks using the following setup:

Infrastructure

Server: AWS c5.2xlarge (8 vCPU, 16GB RAM) Client: AWS c5.xlarge (4 vCPU, 8GB RAM) Network: Same VPC, ~0.1ms latency Database: PostgreSQL 15 (for data-backed tests)

Test Parameters

Concurrent Connections: 10, 50, 100, 500 Requests per Test: 100,000 Payload Sizes: Small (100B), Medium (1KB), Large (100KB)

Tools

REST: FastAPI (Python), wrk (benchmarking) gRPC: grpcio (Python), ghz (benchmarking) GraphQL: Strawberry (Python), k6 (benchmarking)

Benchmark Results

1. Latency Comparison (Small Payload, 100 Concurrent)

┌─────────────┬─────────┬─────────┬─────────┬──────────┐
│  Protocol   │   p50   │   p95   │   p99   │  p99.9   │
├─────────────┼─────────┼─────────┼─────────┼──────────┤
│  gRPC       │  0.8ms  │  1.2ms  │  2.1ms  │   4.5ms  │
│  REST       │  1.4ms  │  2.8ms  │  5.2ms  │  12.3ms  │
│  GraphQL    │  2.1ms  │  4.5ms  │  8.7ms  │  18.6ms  │
└─────────────┴─────────┴─────────┴─────────┴──────────┘

Winner: gRPC (43% faster than REST at p50)

2. Throughput Comparison (Requests/Second)

┌─────────────┬──────────┬──────────┬──────────┐
│  Protocol   │  10 conn │ 100 conn │ 500 conn │
├─────────────┼──────────┼──────────┼──────────┤
│  gRPC       │  15,200  │  45,600  │  52,100  │
│  REST       │   9,800  │  28,400  │  31,200  │
│  GraphQL    │   6,200  │  18,900  │  21,400  │
└─────────────┴──────────┴──────────┴──────────┘

Winner: gRPC (67% higher throughput than REST at 500 connections)

3. Payload Size Impact

┌─────────────┬──────────────────────────────────────┐
│  Protocol   │    Latency by Payload Size (p50)     │
│             │   100B    │    1KB    │   100KB      │
├─────────────┼───────────┼───────────┼──────────────┤
│  gRPC       │   0.8ms   │   1.1ms   │    8.2ms     │
│  REST       │   1.4ms   │   2.1ms   │   18.5ms     │
│  GraphQL    │   2.1ms   │   3.2ms   │   24.1ms     │
└─────────────┴───────────┴───────────┴──────────────┘

Note: gRPC advantage increases with payload size due to binary serialization (Protocol Buffers)

4. Memory Usage Under Load

┌─────────────┬───────────┬───────────┬───────────────┐
│  Protocol   │  Idle MB  │  100 conn │  1000 conn    │
├─────────────┼───────────┼───────────┼───────────────┤
│  gRPC       │    45     │    82     │     156       │
│  REST       │    38     │    95     │     210       │
│  GraphQL    │    52     │   120     │     285       │
└─────────────┴───────────┴───────────┴───────────────┘

Winner: gRPC (most efficient at scale)

Implementation Examples

gRPC Implementation (Python)

// user.proto
syntax = "proto3";

package user;

service UserService { rpc GetUser(GetUserRequest) returns (User); rpc ListUsers(ListUsersRequest) returns (stream User); rpc CreateUser(CreateUserRequest) returns (User); }

message User { int64 id = 1; string name = 2; string email = 3; repeated Order orders = 4; }

message Order { int64 id = 1; double total = 2; string status = 3; }

message GetUserRequest { int64 id = 1; }

message ListUsersRequest { int32 page_size = 1; string page_token = 2; }

message CreateUserRequest { string name = 1; string email = 2; }

server.py

import grpc from concurrent import futures import user_pb2 import user_pb2_grpc

class UserServicer(user_pb2_grpc.UserServiceServicer):

async def GetUser(self, request, context): # Database lookup user = await db.get_user(request.id) if not user: context.set_code(grpc.StatusCode.NOT_FOUND) context.set_details(f'User {request.id} not found') return user_pb2.User()

return user_pb2.User( id=user.id, name=user.name, email=user.email, orders=[ user_pb2.Order(id=o.id, total=o.total, status=o.status) for o in user.orders ] )

async def ListUsers(self, request, context): # Server streaming - yield users one by one async for user in db.stream_users( page_size=request.page_size, page_token=request.page_token ): yield user_pb2.User( id=user.id, name=user.name, email=user.email )

async def serve(): server = grpc.aio.server( futures.ThreadPoolExecutor(max_workers=10), options=[ ('grpc.max_send_message_length', 50 1024 1024), ('grpc.max_receive_message_length', 50 1024 1024), ('grpc.keepalive_time_ms', 10000), ] ) user_pb2_grpc.add_UserServiceServicer_to_server(UserServicer(), server) server.add_insecure_port('[::]:50051') await server.start() await server.wait_for_termination()

REST Implementation (FastAPI)

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import List, Optional

app = FastAPI()

class Order(BaseModel): id: int total: float status: str

class User(BaseModel): id: int name: str email: str orders: List[Order] = []

class CreateUserRequest(BaseModel): name: str email: str

@app.get("/users/{user_id}", response_model=User) async def get_user(user_id: int): user = await db.get_user(user_id) if not user: raise HTTPException(status_code=404, detail="User not found") return user

@app.get("/users", response_model=List[User]) async def list_users( page_size: int = 20, page_token: Optional[str] = None ): return await db.list_users(page_size, page_token)

@app.post("/users", response_model=User, status_code=201) async def create_user(request: CreateUserRequest): return await db.create_user(request.name, request.email)

GraphQL Implementation (Strawberry)

import strawberry
from strawberry.fastapi import GraphQLRouter
from typing import List, Optional

@strawberry.type class Order: id: int total: float status: str

@strawberry.type class User: id: int name: str email: str

@strawberry.field async def orders(self, info) -> List[Order]: # N+1 problem solved with DataLoader return await info.context.order_loader.load(self.id)

@strawberry.type class Query: @strawberry.field async def user(self, id: int) -> Optional[User]: user = await db.get_user(id) if not user: return None return User(id=user.id, name=user.name, email=user.email)

@strawberry.field async def users( self, page_size: int = 20, page_token: Optional[str] = None ) -> List[User]: users = await db.list_users(page_size, page_token) return [User(id=u.id, name=u.name, email=u.email) for u in users]

@strawberry.type class Mutation: @strawberry.mutation async def create_user(self, name: str, email: str) -> User: user = await db.create_user(name, email) return User(id=user.id, name=user.name, email=user.email)

schema = strawberry.Schema(query=Query, mutation=Mutation)

When to Use Each Protocol

Use gRPC When:

✅ Internal microservices communication
✅ High-throughput, low-latency requirements
✅ Streaming data (real-time updates, file transfers)
✅ Polyglot environments (multiple languages)
✅ Mobile backends (bandwidth efficiency)
✅ Server-to-server communication

❌ Browser clients (limited support) ❌ Simple CRUD with few clients ❌ Rapid prototyping

Use REST When:

✅ Public APIs for third-party developers
✅ Browser-based applications
✅ Simple CRUD operations
✅ Caching requirements (HTTP caching)
✅ Wide tooling support needed
✅ Debugging simplicity

❌ High-performance internal services ❌ Complex nested data fetching ❌ Real-time streaming

Use GraphQL When:

✅ Complex, nested data requirements
✅ Multiple client types (web, mobile, IoT)
✅ Rapid frontend iteration
✅ Aggregating multiple services
✅ Reducing over-fetching/under-fetching
✅ Strong typing with flexibility

❌ Simple APIs with fixed contracts ❌ File uploads (primary use case) ❌ Real-time streaming (use subscriptions carefully) ❌ Caching-heavy applications

Hybrid Architecture (Best of All Worlds)

In production, I often use all three:

┌─────────────────────────────────────────────────────────────┐
│                    API Gateway Layer                         │
│                    (Kong / Nginx)                            │
└─────────────────┬─────────────────────┬─────────────────────┘
                  │                     │
    ┌─────────────▼─────────┐  ┌───────▼───────────┐
    │   GraphQL Gateway     │  │    REST Gateway   │
    │   (Public Web/Mobile) │  │  (3rd Party APIs) │
    └─────────────┬─────────┘  └───────┬───────────┘
                  │                     │
    ┌─────────────▼─────────────────────▼─────────────────────┐
    │                  gRPC Service Mesh                       │
    │                  (Internal Communication)                │
    │  ┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────┐    │
    │  │ User    │  │ Order   │  │ Payment │  │ Notif   │    │
    │  │ Service │  │ Service │  │ Service │  │ Service │    │
    │  └─────────┘  └─────────┘  └─────────┘  └─────────┘    │
    └─────────────────────────────────────────────────────────┘

Implementation Example

GraphQL gateway that calls gRPC services

import strawberry import grpc import user_pb2_grpc import order_pb2_grpc

class ServiceClients: def __init__(self): self.user_channel = grpc.aio.insecure_channel('user-service:50051') self.order_channel = grpc.aio.insecure_channel('order-service:50051') self.user_stub = user_pb2_grpc.UserServiceStub(self.user_channel) self.order_stub = order_pb2_grpc.OrderServiceStub(self.order_channel)

clients = ServiceClients()

@strawberry.type class User: id: int name: str email: str

@strawberry.field async def orders(self) -> List['Order']: # GraphQL field calls gRPC service response = await clients.order_stub.ListUserOrders( order_pb2.ListUserOrdersRequest(user_id=self.id) ) return [ Order(id=o.id, total=o.total, status=o.status) for o in response.orders ]

@strawberry.type class Query: @strawberry.field async def user(self, id: int) -> Optional[User]: try: response = await clients.user_stub.GetUser( user_pb2.GetUserRequest(id=id) ) return User( id=response.id, name=response.name, email=response.email ) except grpc.RpcError as e: if e.code() == grpc.StatusCode.NOT_FOUND: return None raise

Performance Optimization Tips

gRPC Optimizations

Connection pooling

channel = grpc.aio.insecure_channel( 'service:50051', options=[ ('grpc.lb_policy_name', 'round_robin'), ('grpc.enable_retries', 1), ('grpc.keepalive_time_ms', 10000), ('grpc.keepalive_timeout_ms', 5000), ('grpc.http2.min_ping_interval_without_data_ms', 5000), ] )

Compression

call_options = [ ('grpc.default_compression_algorithm', grpc.Compression.Gzip), ]

REST Optimizations

Response compression

from fastapi.middleware.gzip import GZipMiddleware app.add_middleware(GZipMiddleware, minimum_size=1000)

Connection keep-alive

import httpx client = httpx.AsyncClient( http2=True, # Enable HTTP/2 limits=httpx.Limits(max_keepalive_connections=100), timeout=httpx.Timeout(10.0, connect=5.0) )

GraphQL Optimizations

DataLoader for N+1 prevention

from strawberry.dataloader import DataLoader

async def load_orders(user_ids: List[int]) -> List[List[Order]]: orders = await db.get_orders_for_users(user_ids) return [ [o for o in orders if o.user_id == uid] for uid in user_ids ]

order_loader = DataLoader(load_fn=load_orders)

Query complexity limiting

from strawberry.extensions import QueryDepthLimiter schema = strawberry.Schema( query=Query, extensions=[QueryDepthLimiter(max_depth=10)] )

Conclusion

After extensive benchmarking and production experience:

| Metric | Winner | Details | |--------|--------|---------| | Raw Latency | gRPC | 40-50% faster than REST | | Throughput | gRPC | 50-70% higher at scale | | Flexibility | GraphQL | Client-driven queries | | Simplicity | REST | Easiest to implement | | Browser Support | REST/GraphQL | gRPC limited | | Streaming | gRPC | Native bidirectional |

My recommendation: Use gRPC for internal services, REST for public APIs, and GraphQL for complex client data needs. The hybrid approach gives you the best of all worlds.

Related Articles

Share this article

Related Articles