Welcome to the fifth lesson in our module on gRPC foundations. In our last session, we traced the entire gRPC request/response lifecycle, from the client stub to the server implementation and back. You now have a solid understanding of how gRPC uses HTTP/2 to transport messages, and how the basic structure of headers, length-prefixed data, and trailers works.
Today, we'll build directly on that knowledge to explore the four distinct communication patterns that this flexible lifecycle enables. Understanding these patterns is fundamental to designing effective and efficient microservices, as each one is suited for different use cases and comes with its own set of trade-offs.
By the end of this lesson, you will be able to explain the four gRPC communication patterns—Unary, Server-streaming, Client-streaming, and Bidirectional-streaming—and know when to apply each one. This is the last major theoretical concept we'll cover before we dive into writing code in the next module.
An Overview of the Four Patterns
At a high level, gRPC communication can be categorized by how many messages the client sends and how many messages the server sends within a single RPC call. This gives us four possible combinations.
The diagram below provides a clear visual summary of these patterns.

Let's briefly break down what you see here:
- Unary RPC: The simplest pattern, functionally equivalent to a standard REST API call or a normal function call. The client sends a single request message and receives a single response message.
- Server Streaming RPC: The client sends a single request message and receives a stream of response messages.
- Client Streaming RPC: The client sends a stream of request messages and receives a single response message once the client finishes sending.
- Bidirectional Streaming RPC: Both the client and the server can send a stream of messages to each other independently. The connection stays open until it's explicitly closed.
Defining Patterns in Your .proto File
As we've discussed, the .proto file is the source of truth for your service. The communication pattern for each RPC method is explicitly defined here using a simple but powerful keyword: stream.
To see how this is done, let's turn to the official gRPC documentation, which uses a Route Guide example service.
Basics tutorial | Python - gRPC
This section from the official gRPC Python tutorial shows exactly how to define each of the four method types in a .proto file.
Please read the section titled "Defining the service". Focus on the four rpc method definitions and notice where the stream keyword is placed for each pattern.
As you saw in the documentation, the syntax is straightforward:
- Unary RPC: No
streamkeyword.rpc GetFeature(Point) returns (Feature) {} - Server-streaming RPC: The
streamkeyword is placed before the response type.rpc ListFeatures(Rectangle) returns (stream Feature) {} - Client-streaming RPC: The
streamkeyword is placed before the request type.rpc RecordRoute(stream Point) returns (RouteSummary) {} - Bidirectional-streaming RPC: The
streamkeyword is placed before both the request and response types.rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
This simple syntax gives you complete control over the communication flow for every method in your service.
Streaming RPCs: Use Cases and Trade-offs
You might be wondering: why use streaming at all? Why not just stick to simple Unary calls? Streaming introduces powerful capabilities for building high-performance systems, but it also brings added complexity. Understanding these trade-offs is crucial for making sound architectural decisions.
The following talk from a Google engineer at a CNCF conference provides an expert perspective on the benefits and challenges of streaming.
Using gRPC for Long-lived and Streaming RPCs - Eric Anderson, Google
This video, "Using gRPC for Long-lived and Streaming RPCs", provides a deep dive into the practical aspects of streaming.
Please watch the following segments: First, watch the brief explanation of Unary RPC as our baseline. Next, focus on the core definitions of the four patterns. Pay close attention to the distinction between half-duplex (one side sends, then the other) and full-duplex (both can send concurrently). Then, learn about the advantages of streaming, such as reducing latency and memory usage, and enabling flow control. Finally, understand the complexities and challenges that come with streaming, like harder retries, potential for deadlocks, and increased API complexity.
Let's distill the key points from the video:
| Pattern | Typical Use Cases | Key Advantages | Production Considerations |
|---|---|---|---|
| Unary | Simple data retrieval (e.g., get user by ID), command execution (e.g., create order). | Simple, predictable, easy to reason about and debug. Works well with standard HTTP proxies. | The default choice. Most of your RPCs will likely be unary. |
| Server Streaming | Subscribing to notifications (e.g., "watch for changes"), returning large datasets in chunks (e.g., paginating search results). | Reduces latency by sending data as it becomes available. Lowers server memory by not needing to buffer the entire response. | The client needs to handle a stream of incoming messages and potential connection failures mid-stream. |
| Client Streaming | Bulk data upload (e.g., sending log files, uploading a large video), sending a stream of sensor data for aggregation. | Reduces client memory usage. Allows the server to process data as it arrives. More efficient than multiple unary calls for large uploads due to less overhead. | The server must be prepared to handle a potentially long-lived stream of incoming data. |
| Bidirectional Streaming | Real-time chat applications, interactive command-line sessions, live location tracking, collaborative editing. | Enables full-duplex, low-latency communication. Most flexible pattern. Allows for complex, stateful interactions over a single connection. | Most complex pattern. Requires careful state management on both client and server. High risk of deadlocks if not handled correctly. Retries must be implemented at the application level. |
Choosing the right pattern is a design trade-off between performance, efficiency, and implementation complexity.
A Glimpse into Implementation
While we'll dive deep into coding in the next module, it's helpful to see how these patterns translate into Python code. The distinction often comes down to a simple difference in how you handle requests and responses: returning a single value versus yielding a stream of values.
Let's look at the server-side implementation skeletons from the gRPC documentation.
1. Unary RPC
The server receives a single request object and uses return to send a single response.
# proto: rpc GetFeature(Point) returns (Feature) {}
def GetFeature(self, request, context):
# ... logic to find the feature ...
return feature_object
2. Server Streaming RPC
The server receives a single request object but uses yield to send multiple response objects.
# proto: rpc ListFeatures(Rectangle) returns (stream Feature) {}
def ListFeatures(self, request, context):
# ... logic to find features within the rectangle ...
for feature in found_features:
yield feature
3. Client Streaming RPC
The server is given a request_iterator. It processes the stream of incoming requests and then uses return to send a single, final response.
# proto: rpc RecordRoute(stream Point) returns (RouteSummary) {}
def RecordRoute(self, request_iterator, context):
# ... logic to process incoming points ...
for point in request_iterator:
# process each point
# ... create summary ...
return route_summary_object
4. Bidirectional Streaming RPC
The server is given a request_iterator and also uses yield to send its own stream of responses. The two streams are independent.
# proto: rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
def RouteChat(self, request_iterator, context):
for new_note in request_iterator:
# Process the incoming note
# ... logic to find related notes ...
for prev_note in related_notes:
yield prev_note # Send a response
This contrast between return for single responses and yield/iterators for streams is the core difference you'll encounter when implementing these patterns in Python.
Conclusion
In this lesson, you have learned about the four fundamental communication patterns in gRPC. You can now identify them, define them in a .proto file, and explain their respective use cases and trade-offs.
Key Takeaways:
- gRPC supports four communication patterns: Unary (1:1), Server Streaming (1:N), Client Streaming (N:1), and Bidirectional Streaming (N:N).
- The
streamkeyword in the.protoservice definition determines the communication pattern for an RPC method. - Unary is the simplest and most common pattern, ideal for standard request-response interactions.
- Streaming patterns offer significant performance and efficiency benefits for specific use cases (like notifications, bulk uploads, or real-time chat) but introduce greater implementation complexity and require careful handling of state and errors.
- The choice of communication pattern is a critical system design decision that balances performance requirements against development complexity.
Preview of the Next Lesson
We have now concluded our deep dive into the foundations of Protobuf and gRPC. You have all the theoretical knowledge needed to start building.
In our next lesson, we will officially begin Module 6: Building High-Performance gRPC Services. You will get your hands dirty by using the grpc_tools compiler to generate Python code from a .proto file and then write the code for your very first async gRPC service and client. It's time to put theory into practice.