Skip to content

gRPC Unary RPC — Simple Request-Response Communication

DodaTech Updated 2026-06-28 6 min read

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

  1. Not setting deadlines: Without deadlines, a stalled gRPC call blocks resources indefinitely. Always set a timeout on every call.

  2. Ignoring error codes: gRPC has rich error codes (INVALID_ARGUMENT, NOT_FOUND, UNAVAILABLE). Map them to appropriate HTTP status codes when bridging to REST.

  3. Blocking the event loop: gRPC calls are synchronous by default. In async frameworks, use the async gRPC API to avoid blocking the event loop.

  4. Not reusing channels: Creating a new gRPC channel per request wastes connections. Reuse channels and stubs across requests.

  5. Missing error details: Use context.set_details() and context.send_initial_metadata() to provide rich error information. Generic errors make debugging impossible.

Practice Questions

  1. What is a unary RPC? A pattern where the client sends one request and the server replies with one response, like a function call.

  2. 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.

  3. What happens when a client does not set a deadline? The call may hang indefinitely if the server is slow or unresponsive, leaking resources.

  4. How do you reuse gRPC connections efficiently? Create a single channel and stub per service endpoint and reuse them across requests.

  5. 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

What is the difference between unary and server streaming?

Unary sends one request and receives one response. Server streaming sends one request and receives a stream of responses over time.

Can unary gRPC return errors?

Yes. Use context.set_code() and context.set_details() to return structured errors matching gRPC status codes.

How do unary calls compare to REST in performance?

Unary gRPC is typically 5-10x faster than REST/JSON due to binary protobuf serialization and HTTP/2 multiplexing.

Should I use unary or streaming for file uploads?

For small files, unary works. For large files, use client streaming to send chunks without loading the entire file into memory.

Can unary calls be canceled?

Yes. The client can cancel a unary call via the context. The server can detect cancellation with context.is_active().

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