Three ideas you already know, bolted together: Protobuf for the bytes, HTTP/2 for the pipe, and generated stubs that make a network call look like a method call.
gRPC is not one new thing — it's an assembly of parts that each have their own guide on this site:
protoc compiler reads your .proto and generates client stubs and server base classes in your language.Reading top to bottom, this is what a gRPC call is made of:
Each layer only knows about the one below it. Your business code sees a method; the wire sees TCP bytes.
That separation is exactly why the same .proto generates working clients in Go, Python, Java, and Rust with no hand-written wire code.
.proto FileEverything starts from an interface definition. You declare messages (the data) and a service (the callable methods):
syntax = "proto3";
package users.v1;
message GetUserRequest {
string user_id = 1;
}
message User {
string user_id = 1;
string display_name = 2;
string email = 3;
}
service UserService {
// A plain request/response method (unary).
rpc GetUser(GetUserRequest) returns (User);
}
Run the compiler and you get two generated artifacts per language:
# Generate Python code from the contract
# python -m grpc_tools.protoc -I. \
# --python_out=. --grpc_python_out=. users/v1/user.proto
# -> user_pb2.py (the message classes: GetUserRequest, User)
# -> user_pb2_grpc.py (UserServiceStub + UserServiceServicer)
rpc. You call it; it does the marshalling and networking.The channel owns the HTTP/2 connection(s), name resolution, load-balancing, and connection state. Create it once and reuse it:
import grpc
from users.v1 import user_pb2, user_pb2_grpc
# One channel, long-lived, shared across the process.
channel = grpc.insecure_channel("users.internal:50051")
stub = user_pb2_grpc.UserServiceStub(channel) # cheap wrapper
resp = stub.GetUser(user_pb2.GetUserRequest(user_id="u_123"))
print(resp.display_name)
Subclass the generated servicer, implement each method, and register it:
import grpc
from concurrent import futures
from users.v1 import user_pb2, user_pb2_grpc
class UserService(user_pb2_grpc.UserServiceServicer):
def GetUser(self, request, context):
user = db.load(request.user_id) # your logic
return user_pb2.User(
user_id=user.id,
display_name=user.name,
email=user.email,
)
server = grpc.server(futures.ThreadPoolExecutor(max_workers=16))
user_pb2_grpc.add_UserServiceServicer_to_server(UserService(), server)
server.add_insecure_port("[::]:50051")
server.start()
server.wait_for_termination()
Every handler receives a context — the door to deadlines, cancellation, metadata, and status codes. It shows up in every other guide in this collection.
A gRPC call is a perfectly ordinary HTTP/2 request/response — with conventions:
:path: /package.Service/Method.grpc-status), not the HTTP :status — HTTP is almost always 200 even for failures. More in Status Codes.DATA frames. See Head-of-Line Blocking for the one place this still bites.
| Piece | Role | Guide |
|---|---|---|
| Protobuf | Serializes messages to compact binary | Protobuf Basics |
| HTTP/2 | Multiplexed, full-duplex transport | HTTP/2 |
.proto | The shared contract → codegen | This page |
| Channel | Long-lived connection, made once | This page |
| Stub / Servicer | Generated client / server surfaces | This page |