gRPC Unary RPC — Simple Request-Response Communication
In this tutorial, you will learn about grpc unary rpc. We cover key concepts, practical examples, and best practices to help you master this topic.
gRPC unary RPC is the simplest communication pattern where a client sends one request message and receives one response message, similar to a traditional REST call but with binary protobuf Serialization.
What You'll Learn
By the end of this lesson you will define a unary RPC in a .proto file, implement a gRPC server in Python, write a client that calls the unary method, handle errors and deadlines, and understand when to use unary vs streaming.
Why It Matters
Unary RPC is the foundation of most gRPC services. It replaces REST endpoints for service-to-service communication with strongly typed, faster binary messages. Mastering unary calls lets you migrate internal REST APIs to gRPC for performance gains.
Real-World Use
DodaZIP's user authentication service uses unary gRPC calls to validate tokens. The API Gateway sends a ValidateToken request containing the JWT, and the auth service returns the user ID and permissions in a single response, all in under 2ms.
flowchart LR
A[Client] -->|Unary Request| B[gRPC Server]
B -->|Unary Response| A
subgraph Protobuf
C[Request Message]
D[Response Message]
end
C --> A
D --> B
style B fill:#2d3748,color:#fff
Defining a Unary RPC
The .proto definition for a unary call.
# unary_proto.py
# Unary RPC definition
def unary_proto():
print("Unary RPC Proto Definition")
print("=" * 40)
print()
print('syntax = "proto3";')
print()
print("package calculator.v1;")
print()
print('service Calculator {')
print(" rpc Add (AddRequest) returns (AddResponse);")
print(" rpc Divide (DivideRequest) returns (DivideResponse);")
print("}")
print()
print("message AddRequest {")
print(" int32 a = 1;")
print(" int32 b = 2;")
print("}")
print()
print("message AddResponse {")
print(" int32 result = 1;")
print("}")
print()
print("message DivideRequest {")
print(" int32 dividend = 1;")
print(" int32 divisor = 2;")
print("}")
print()
print("message DivideResponse {")
print(" int32 quotient = 1;")
print(" int32 remainder = 2;")
print("}")
unary_proto()
Implementing the Server
Python gRPC server for unary calls.
# grpc_server.py
# gRPC unary server implementation
def server_implementation():
print("gRPC Unary Server Implementation")
print("=" * 40)
print()
server_code = """
import grpc
from concurrent import futures
import calculator_pb2
import calculator_pb2_grpc
class CalculatorServicer(calculator_pb2_grpc.CalculatorServicer):
def Add(self, request, context):
result = request.a + request.b
return calculator_pb2.AddResponse(result=result)
def Divide(self, request, context):
if request.divisor == 0:
context.set_code(grpc.StatusCode.INVALID_ARGUMENT)
context.set_details("Division by zero is not allowed")
return calculator_pb2.DivideResponse()
quotient = request.dividend // request.divisor
remainder = request.dividend % request.divisor
return calculator_pb2.DivideResponse(
quotient=quotient, remainder=remainder
)
def serve():
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
calculator_pb2_grpc.add_CalculatorServicer_to_server(
CalculatorServicer(), server
)
server.add_insecure_port('[::]:50051')
server.start()
server.wait_for_termination()
if __name__ == '__main__':
serve()
"""
print(server_code)
print("Key points:")
print("- Servicer class implements the RPC methods")
print("- context.set_code() for error signaling")
print("- ThreadPoolExecutor handles concurrent requests")
server_implementation()
Implementing the Client
Calling the unary RPC from a client.
# grpc_client.py
# gRPC unary client
def client_implementation():
print("gRPC Unary Client Implementation")
print("=" * 40)
print()
client_code = """
import grpc
import calculator_pb2
import calculator_pb2_grpc
def run():
channel = grpc.insecure_channel('localhost:50051')
stub = calculator_pb2_grpc.CalculatorStub(channel)
# Simple addition
response = stub.Add(
calculator_pb2.AddRequest(a=10, b=25)
)
print(f"10 + 25 = {response.result}")
# Division with error handling
try:
response = stub.Divide(
calculator_pb2.DivideRequest(dividend=10, divisor=0)
)
except grpc.RpcError as e:
print(f"Error: {e.code()}: {e.details()}")
# Successful division
response = stub.Divide(
calculator_pb2.DivideRequest(dividend=17, divisor=5)
)
print(f"17 / 5 = {response.quotient} remainder {response.remainder}")
if __name__ == '__main__':
run()
"""
print(client_code)
print()
print("Expected output:")
print(" 10 + 25 = 35")
print(" Error: StatusCode.INVALID_ARGUMENT: Division by zero is not allowed")
print(" 17 / 5 = 3 remainder 2")
client_implementation()
Deadlines and Timeouts
Preventing unary calls from hanging indefinitely.
# deadlines.py
# gRPC deadline and timeout handling
def deadline_handling():
print("gRPC Deadline and Timeout Handling")
print("=" * 40)
print()
code = """
import grpc
import user_pb2
import user_pb2_grpc
def fetch_user_with_timeout():
channel = grpc.insecure_channel('user-service:50051')
stub = user_pb2_grpc.UserServiceStub(channel)
try:
# Deadline of 500ms
response = stub.GetUser(
user_pb2.GetUserRequest(user_id="123"),
timeout=0.5
)
return response
except grpc.RpcError as e:
if e.code() == grpc.StatusCode.DEADLINE_EXCEEDED:
print("User service took too long, proceeding with fallback")
return None
raise
result = fetch_user_with_timeout()
"""
print(code)
print()
print("Deadline propagation:")
print("- Client sets timeout=0.5 (500ms)")
print("- If server exceeds deadline, DEADLINE_EXCEEDED returned")
print("- Deadline propagates to downstream gRPC calls automatically")
deadline_handling()
Common Mistakes
Not setting deadlines: Without deadlines, a stalled gRPC call blocks resources indefinitely. Always set a timeout on every call.
Ignoring error codes: gRPC has rich error codes (INVALID_ARGUMENT, NOT_FOUND, UNAVAILABLE). Map them to appropriate HTTP status codes when bridging to REST.
Blocking the event loop: gRPC calls are synchronous by default. In async frameworks, use the async gRPC API to avoid blocking the event loop.
Not reusing channels: Creating a new gRPC channel per request wastes connections. Reuse channels and stubs across requests.
Missing error details: Use context.set_details() and context.send_initial_metadata() to provide rich error information. Generic errors make debugging impossible.
Practice Questions
What is a unary RPC? A pattern where the client sends one request and the server replies with one response, like a function call.
How do you handle errors in a gRPC unary server? Use context.set_code() with a gRPC status code and context.set_details() with a message.
What happens when a client does not set a deadline? The call may hang indefinitely if the server is slow or unresponsive, leaking resources.
How do you reuse gRPC connections efficiently? Create a single channel and stub per service endpoint and reuse them across requests.
Challenge: Implement a unary gRPC service for URL shortening. Define the .proto with Shorten and Resolve methods, implement the server with storage, and write a client that tests both success and error cases.
FAQ
Mini Project
Build a unary gRPC service for a URL shortener. Define a .proto with Shorten and Resolve RPCs, implement the server with an in-memory store, and write a client that shortens URLs and resolves them. Include error handling for invalid URLs and missing short codes.
def url_shortener():
print("gRPC URL Shortener Service")
print("=" * 40)
print()
print('syntax = "proto3";')
print()
print("package urlshortener.v1;")
print()
print('service UrlShortener {')
print(" rpc Shorten (ShortenRequest) returns (ShortenResponse);")
print(" rpc Resolve (ResolveRequest) returns (ResolveResponse);")
print("}")
print()
print("message ShortenRequest {")
print(" string long_url = 1;")
print("}")
print()
print("message ShortenResponse {")
print(" string short_code = 1;")
print(" string short_url = 2;")
print("}")
print()
print("message ResolveRequest {")
print(" string short_code = 1;")
print("}")
print()
print("message ResolveResponse {")
print(" string long_url = 1;")
print("}")
print()
print("Flow: Client Shorten -> Server generates 6-char code")
print(" Client Resolve -> Server returns original URL")
url_shortener()
What's Next
Next: gRPC Streaming for server-side streaming RPC patterns.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro