When building backend systems, choosing the right communication protocol can have a significant impact on performance, maintainability, and scalability.
REST is still the most common choice for public APIs, while GraphQL has become popular when clients need flexible data fetching.
But when services need to communicate efficiently with each other, another approach becomes particularly interesting: RPC, and more specifically gRPC.
In this article, we'll explore:
- What RPC actually means
- How gRPC works
- What Protocol Buffers are
- How to build a gRPC service with Python
- gRPC vs REST vs GraphQL
- When you should use each approach
1. What is RPC?
RPC stands for Remote Procedure Call.
The idea is simple: instead of thinking in terms of HTTP resources, we think in terms of calling a method on a remote service.
For example, with a traditional REST API, we might have:
GET /users/42
With RPC, the mental model is closer to:
UserService.GetUser(42)
The client calls a method, and the server executes that method remotely.
Of course, the method isn't actually local. The RPC framework takes care of the communication between the client and the server.
gRPC follows this model and allows a client application to call methods on another machine almost like calling local methods.
2. What is gRPC?
gRPC is an RPC framework originally developed at Google.
It allows services to communicate using strongly defined service contracts.
A typical gRPC architecture looks like this:
┌─────────────┐
│ Client │
│ Python │
└──────┬──────┘
│
│ gRPC
│
▼
┌─────────────────┐
│ Server │
│ Python │
└─────────────────┘
Instead of manually designing HTTP endpoints and JSON payloads, we define the service contract in a .proto file.
gRPC can use Protocol Buffers as both the interface definition language and the message format.
3. Protocol Buffers: the contract
Protocol Buffers, commonly called Protobuf, are used to define the messages and services exchanged between applications.
For example:
syntax = "proto3";
service UserService {
rpc GetUser(GetUserRequest) returns (User);
}
message GetUserRequest {
int32 id = 1;
}
message User {
int32 id = 1;
string name = 2;
string email = 3;
}
This file describes our API contract.
We have:
- A
UserService - A
GetUserRPC method - A
GetUserRequest - A
Userresponse
The important part is that the contract is language-independent.
A Python service could communicate with a Go, Java, C#, or Node.js service using the same .proto definition.
gRPC officially supports multiple languages, including Python, Go, Java, C#, Node.js, Ruby, PHP and others.
4. Building a gRPC API with Python
Let's build a very small service.
Install the dependencies
python -m pip install grpcio grpcio-tools
The official Python gRPC documentation uses grpcio for the runtime and grpcio-tools for generating Python code from .proto definitions.
Our project could look like this:
grpc-demo/
│
├── proto/
│ └── user.proto
│
├── server.py
├── client.py
└── requirements.txt
5. Define the service
Create:
proto/user.proto
with:
syntax = "proto3";
package user;
service UserService {
rpc GetUser(GetUserRequest) returns (User);
}
message GetUserRequest {
int32 id = 1;
}
message User {
int32 id = 1;
string name = 2;
string email = 3;
}
We have now defined our API contract.
6. Generate Python code
We can generate the Python client/server code with:
python -m grpc_tools.protoc \
-I./proto \
--python_out=. \
--grpc_python_out=. \
./proto/user.proto
This generates files similar to:
user_pb2.py
user_pb2_grpc.py
The first contains the generated Protocol Buffer message classes, while the second contains the gRPC-specific client and server code.
You normally don't edit these generated files manually.
7. Implement the Python server
Now we can implement the service.
from concurrent import futures
import grpc
import user_pb2
import user_pb2_grpc
class UserService(user_pb2_grpc.UserServiceServicer):
def GetUser(self, request, context):
return user_pb2.User(
id=request.id,
name="John Doe",
email="john@example.com",
)
def serve():
server = grpc.server(
futures.ThreadPoolExecutor(max_workers=10)
)
user_pb2_grpc.add_UserServiceServicer_to_server(
UserService(),
server,
)
server.add_insecure_port("[::]:50051")
server.start()
print("gRPC server running on port 50051")
server.wait_for_termination()
if __name__ == "__main__":
serve()
The generated UserServiceServicer provides the interface that our Python implementation extends.
The official gRPC Python examples follow the same general pattern: create a server, register the generated servicer, bind a port, and start the server.
8. Create the Python client
Now let's consume the service.
import grpc
import user_pb2
import user_pb2_grpc
def main():
with grpc.insecure_channel("localhost:50051") as channel:
client = user_pb2_grpc.UserServiceStub(channel)
response = client.GetUser(
user_pb2.GetUserRequest(id=42)
)
print(response)
if __name__ == "__main__":
main()
The interesting part is:
client.GetUser(...)
From the developer's perspective, it looks very similar to calling a normal Python method.
Behind the scenes, however, the request is sent to another process or machine.
The gRPC Python API generates a client stub from the .proto service definition.
9. gRPC supports streaming
One of the features that makes gRPC particularly interesting for backend systems is streaming.
There are four RPC patterns:
Unary RPC
One request → one response.
Client ── Request ──> Server
Client <── Response ─ Server
Example:
rpc GetUser(GetUserRequest) returns (User);
Server streaming
One request → multiple responses.
rpc ListUsers(ListUsersRequest) returns (stream User);
Useful when a server needs to continuously send data to a client.
Client streaming
Multiple requests → one response.
rpc UploadUsers(stream User) returns (UploadSummary);
Useful for sending a stream of data to the server.
Bidirectional streaming
Multiple requests ↔ multiple responses.
rpc Chat(stream Message) returns (stream Message);
Both sides can continuously exchange messages.
gRPC's Python documentation demonstrates all four communication patterns.
10. gRPC vs REST
So when should we use gRPC instead of REST?
Let's compare them.
| gRPC | REST | |
|---|---|---|
| Protocol | HTTP/2 | Usually HTTP/1.1 or HTTP/2 |
| Data format | Protobuf | Usually JSON |
| Contract | .proto |
OpenAPI often used |
| Code generation | Built-in | Optional |
| Streaming | Excellent | More limited |
| Browser support | More complex | Excellent |
| Human-readable payload | No | Yes |
| Microservices | Excellent | Excellent |
| Public APIs | Less common | Excellent |
REST is extremely convenient for public APIs.
For example:
GET /users/42
is easy to understand and easy to consume using:
curl
A JSON response is also easy to inspect:
{
"id": 42,
"name": "John Doe",
"email": "john@example.com"
}
This simplicity is one of REST's biggest advantages.
11. gRPC vs GraphQL
GraphQL solves a different problem.
Instead of exposing multiple REST endpoints, GraphQL usually exposes a schema through which clients can request exactly the fields they need.
For example:
query {
user(id: 42) {
name
email
}
}
This is extremely useful when different clients have different data requirements.
For example:
Mobile App
│
▼
GraphQL
│
┌───┼─────────┐
▼ ▼ ▼
User Order Product
Service Service
GraphQL is particularly attractive for frontend-facing APIs.
gRPC, on the other hand, is often a better fit when backend services need a strongly typed and efficient service-to-service contract.
12. Which one should you choose?
There isn't a universal winner.
Choose REST when:
- You are building a public API
- Browser compatibility matters
- Simplicity is important
- Human-readable JSON is useful
- You want a familiar HTTP interface
Choose GraphQL when:
- Clients need different subsets of data
- You have complex frontend requirements
- You want clients to control the shape of responses
- You are building a frontend-facing aggregation layer
Choose gRPC when:
- You are building microservices
- You control both client and server
- Strong contracts are important
- Performance matters
- You need streaming
- You have multiple programming languages in your architecture
A common production architecture can even combine them.
For example:
Internet
│
▼
┌───────────┐
│ REST/API │
│ Gateway │
└─────┬─────┘
│
┌─────▼─────┐
│ User │
│ Service │
└─────┬─────┘
│ gRPC
┌─────────┼──────────┐
▼ ▼ ▼
Orders Payments Products
Service Service Service
REST can be the external interface while gRPC handles internal service-to-service communication.
13. The biggest advantage: a contract-first approach
For me, one of the strongest advantages of gRPC isn't simply performance.
It's the contract.
With:
service UserService {
rpc GetUser(GetUserRequest) returns (User);
}
the API contract is explicit.
From the same definition, tooling can generate client/server code.
This reduces a class of problems that can happen when API contracts are maintained manually.
Protocol Buffers also provide generated Python classes rather than requiring developers to manually serialize and deserialize every message.
14. But gRPC isn't always the right choice
There are trade-offs.
gRPC can be more complex than a simple REST API.
For example, debugging this:
Client
│
▼
gRPC
│
HTTP/2
│
Protobuf
│
Server
can be less convenient than inspecting a JSON REST request.
Also, browser clients typically require additional considerations such as gRPC-Web rather than directly using the standard gRPC protocol.
So choosing gRPC simply because it is "faster" is not enough.
The architecture and communication requirements should drive the decision.
15. Final thoughts
As a Python Backend Developer, understanding gRPC is valuable because modern distributed systems often involve multiple services communicating with each other.
REST remains an excellent choice for many APIs.
GraphQL is powerful when clients need flexible access to data.
gRPC becomes particularly interesting for internal service-to-service communication where strong contracts, generated code, and streaming are important.
The key lesson is:
Don't choose a protocol because it is popular. Choose it because it fits the communication problem you're solving.
For a Python backend engineer, knowing REST + GraphQL + gRPC gives you a much broader understanding of API architecture than knowing only how to build CRUD endpoints.
Resources
- gRPC Python documentation
- gRPC Python Quick Start
- gRPC Python Basics
- Protocol Buffers — Python Generated Code
About the author
I'm a Python Backend Developer interested in distributed systems, APIs, microservices, databases, and backend architecture.
I'm documenting what I learn while building backend systems and exploring technologies such as Python, FastAPI, PostgreSQL, Docker, gRPC, and cloud infrastructure.
Top comments (0)