21 Understanding the Concept of Streaming in gRPC
If you’re already familiar with REST APIs, the request-response communication model probably feels very natural. However, as applications grow more complex and the need for efficient two-way communication arises—such as real-time chat, system monitoring, or transferring large files—REST starts to show its limitations. This is exactly where the power of gRPC, and its streaming feature in particular, truly stands out.
In this article, we’ll dissect the concept of streaming in gRPC. We’ll understand how it works, explore the different types of streaming gRPC offers, and put it into easy-to-digest Go code examples. And of course, we’ll also include simulations, flow diagrams, and tables to make the concept stick firmly in your mind.
What Is Streaming in gRPC?
Put simply, streaming in gRPC is a mechanism in which the client and/or server can send a series of messages rather than just a single request/response. In other words, data can be ‘sent incrementally’ between the client and server, either asynchronously or synchronously, without having to wait for all of the data to be ready on the sender’s side.
Why Do We Need Streaming?
A few of the use cases that benefit most from streaming:
- Large file transfers: Splitting a large file into chunks that are sent incrementally.
- Real-time chat: Messages can flow continuously in both directions.
- Live data feeds: Streaming server monitors, transaction dashboards, and so on.
Types of Streaming in gRPC
There are four kinds of RPC methods in gRPC:
| Type | Client Stream | Server Stream | Example Scenario |
|---|---|---|---|
| Unary | ❌ | ❌ | Request-Response |
| Server Streaming | ❌ | ✅ | Server monitoring |
| Client Streaming | ✅ | ❌ | File chunk upload |
| Bidirectional | ✅ | ✅ | Live chat |
Let’s take a look at the mermaid diagram below to visualize it:
flowchart LR
A[Unary] --> B1[Client Request] & B2[Server Response]
C[Server Streaming] --> D1[Client Request] --> D2[Server Stream Data]
E[Client Streaming] --> F1[Client Stream Data] --> F2[Server Response]
G[Bidirectional] --> H1[Client Stream] <--> H2[Server Stream]
1. Unary RPC (Request-Response)
The simplest form: one request is answered with one response. There is no streaming. It typically looks like this in Protobuf:
1service FileService {
2 rpc GetFileMetadata(FileRequest) returns (FileMetadata) {}
3}2. Server Streaming RPC
The client sends a single request, and the server responds with a series of streamed data. Example use case: a dashboard that wants to display server status updates periodically.
1service MonitorService {
2 rpc MonitorStatus(Empty) returns (stream Status) {}
3}How it works:
- The client sends a request.
- The server sends data/responses multiple times until it’s done.
- The client reads the data stream until it’s exhausted.
Go Code Example (Server Streaming)
Protobuf definition:
1syntax = "proto3";
2
3service MonitorService {
4 rpc MonitorStatus(Empty) returns (stream Status) {}
5}
6
7message Status {
8 string server = 1;
9 string state = 2;
10 int32 cpuUsage = 3;
11}
12
13message Empty {}Go server:
1func (s *server) MonitorStatus(req *Empty, stream MonitorService_MonitorStatusServer) error {
2 for i := 0; i < 5; i++ {
3 status := &Status{
4 Server: "MyApp",
5 State: "UP",
6 CpuUsage: rand.Int31n(100),
7 }
8 if err := stream.Send(status); err != nil {
9 return err
10 }
11 time.Sleep(2 * time.Second)
12 }
13 return nil
14}Go client:
1stream, _ := client.MonitorStatus(context.Background(), &Empty{})
2for {
3 status, err := stream.Recv()
4 if err == io.EOF {
5 break
6 }
7 fmt.Printf("Status: %+v\n", status)
8}3. Client Streaming RPC
The client can send many messages to the server, and the server replies with a single response after all of the messages have been received.
Use case: Sending the pieces (chunks) of a file to be uploaded.
Protobuf:
1service FileService {
2 rpc Upload(stream FileChunk) returns (UploadStatus) {}
3}
4
5message FileChunk {
6 bytes content = 1;
7}
8
9message UploadStatus {
10 string message = 1;
11}Go server:
1func (s *server) Upload(stream FileService_UploadServer) error {
2 var totalBytes int
3 for {
4 chunk, err := stream.Recv()
5 if err == io.EOF {
6 return stream.SendAndClose(&UploadStatus{Message: fmt.Sprintf("Received %d bytes", totalBytes)})
7 }
8 totalBytes += len(chunk.Content)
9 }
10}Go client:
1stream, _ := client.Upload(context.Background())
2for i := 0; i < 10; i++ {
3 stream.Send(&FileChunk{Content: []byte("part")})
4}
5resp, _ := stream.CloseAndRecv()
6fmt.Println(resp.Message)4. Bidirectional Streaming
Both the client and the server can send streamed data to each other at any time and in any amount.
Use case: Real-time chat, collaborative editing, and so on.
Protobuf:
1service ChatService {
2 rpc Chat(stream ChatMessage) returns (stream ChatMessage) {}
3}
4
5message ChatMessage {
6 string user = 1;
7 string text = 2;
8 int64 timestamp = 3;
9}Go server:
1func (s *server) Chat(stream ChatService_ChatServer) error {
2 for {
3 msg, err := stream.Recv()
4 if err == io.EOF {
5 return nil
6 }
7 // broadcast to other users (simplified)
8 stream.Send(&ChatMessage{
9 User: "Server",
10 Text: "Received: " + msg.Text,
11 Timestamp: time.Now().Unix(),
12 })
13 }
14}Go client:
1ctx := context.Background()
2stream, _ := client.Chat(ctx)
3go func() {
4 for _, text := range []string{"Hi", "Apa kabar?"} {
5 stream.Send(&ChatMessage{User: "Alice", Text: text, Timestamp: time.Now().Unix()})
6 time.Sleep(time.Second)
7 }
8 stream.CloseSend()
9}()
10for {
11 msg, err := stream.Recv()
12 if err == io.EOF {
13 break
14 }
15 fmt.Printf("Received: %s\n", msg.Text)
16}Time & Efficiency Simulation
Let’s compare uploading a 100MB file using a unary RPC versus client streaming:
| Method | Chunk Size | Total Requests | Latency per Req | Est. Total Time |
|---|---|---|---|---|
| Unary (100MB at once) | - | 1 | 5s | 5s |
| Streaming (10MB) | 10MB | 10 | 0.5s | 5s |
| Streaming pipeline | 10MB | 10 | parallel (0.5s) | ~0.5s |
With streaming, chunks can be sent incrementally and in a pipeline, so as long as there’s enough bandwidth, it’s faster and uses resources more efficiently.
Conclusion
gRPC Streaming opens up far more flexible, efficient, and real-time network communication compared to conventional (unary) RPC. Here are the key takeaways:
- Use server streaming for broadcasting/monitoring.
- Choose client streaming to upload large amounts of data incrementally.
- Leverage bidirectional streaming for real-time two-way use cases.
- Streaming is more efficient than unary for large or repetitive data.
This technology will only grow more crucial as applications shift toward being interactive, event-driven, and data intensive.
Hopefully the explanations, code, and diagrams above leave you feeling much more confident about streaming in gRPC. Ready to implement it in your production systems? 🚀