22 Implementing Server-side Streaming RPC
gRPC has become one of the most widely used communication protocols in the modern microservices ecosystem. Its role as a replacement for REST in many systems stems from the flexibility of the RPC (Remote Procedure Call) model, its speed, and its superior data streaming capabilities. One of the more powerful streaming models is Server-side Streaming RPC.
In this article, I’ll walk through the concept of Server-side Streaming RPC, the steps to implement it, and a case study using Go (golang). To make things easier to follow, there will be code examples, a request-response flow simulation, and a simple comparison table against the other RPC models.
What Is Server-side Streaming RPC?
In the Server-side Streaming RPC scenario, the client sends a single request to the server and receives a stream of responses incrementally from the server until it completes. In short, the server can send more than one message back to the client after receiving the initial request—similar to a “push notification” over HTTP/2.
Flow Diagram
Let’s visualize the flow using a mermaid diagram:
sequenceDiagram
participant Client
participant Server
Client->>Server: Send Request (permintaan tunggal)
loop Selama masih ada data
Server-->>Client: Stream Response (berulang kali)
end
Server-->>Client: (Stream selesai / EOF)
When Should You Use Server-side Streaming RPC?
Some typical use cases include:
- Sending data in large batches (reports, files, logs)
- Streaming database query results incrementally
- Push notifications about the status or updates of background processes
- Event-driven communication (e.g., build pipeline progress)
The following table compares the RPC models in gRPC:
| RPC Type | Request | Response | Example |
|---|---|---|---|
| Unary RPC | One | One | GetUser() -> User |
| Server-side Streaming | One | Stream (many) | ListLogs() -> LogEntry* |
| Client-side Streaming | Stream (many) | One | UploadPhotos() -> UploadStatus |
| Bidirectional Streaming | Stream | Stream (many, two-way) | Chat() -> Message*/Message* |
Case Study: Streaming Logs with gRPC in Go
We’ll build a simple gRPC service called LogService, where the client can request logs and the server responds with a stream of log data incrementally.
1. Defining the Protobuf
File: log.proto
1syntax = "proto3";
2
3package log;
4
5service LogService {
6 rpc FetchLogs(LogRequest) returns (stream LogEntry) {}
7}
8
9message LogRequest {
10 string filter = 1;
11}
12
13message LogEntry {
14 int32 id = 1;
15 string timestamp = 2;
16 string message = 3;
17}2. Server Implementation
File: server.go
1package main
2
3import (
4 "context"
5 "fmt"
6 "log"
7 "net"
8 "time"
9
10 "google.golang.org/grpc"
11
12 pb "path/to/your/generated/proto/log"
13)
14
15type server struct {
16 pb.UnimplementedLogServiceServer
17}
18
19func (s *server) FetchLogs(req *pb.LogRequest, stream pb.LogService_FetchLogsServer) error {
20 logs := []pb.LogEntry{
21 {Id: 1, Timestamp: "2024-06-05T10:00:00Z", Message: "Server started"},
22 {Id: 2, Timestamp: "2024-06-05T10:10:00Z", Message: "Process completed"},
23 {Id: 3, Timestamp: "2024-06-05T10:15:00Z", Message: "User login"},
24 }
25
26 for _, entry := range logs {
27 // Simulate logs being sent incrementally
28 if err := stream.Send(&entry); err != nil {
29 return err
30 }
31 time.Sleep(1 * time.Second) // Simulate streaming delay
32 }
33 return nil
34}
35
36func main() {
37 lis, err := net.Listen("tcp", ":50051")
38 if err != nil {
39 log.Fatalf("failed to listen: %v", err)
40 }
41 s := grpc.NewServer()
42 pb.RegisterLogServiceServer(s, &server{})
43 log.Println("gRPC server started at :50051")
44 if err := s.Serve(lis); err != nil {
45 log.Fatalf("failed to serve: %v", err)
46 }
47}3. Client Implementation
File: client.go
1package main
2
3import (
4 "context"
5 "log"
6 "time"
7
8 "google.golang.org/grpc"
9
10 pb "path/to/your/generated/proto/log"
11)
12
13func main() {
14 conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
15 if err != nil {
16 log.Fatalf("did not connect: %v", err)
17 }
18 defer conn.Close()
19 c := pb.NewLogServiceClient(conn)
20
21 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
22 defer cancel()
23 stream, err := c.FetchLogs(ctx, &pb.LogRequest{Filter: "all"})
24 if err != nil {
25 log.Fatalf("Error on FetchLogs: %v", err)
26 }
27
28 for {
29 entry, err := stream.Recv()
30 if err != nil {
31 log.Printf("End of stream or error: %v", err)
32 break
33 }
34 log.Printf("LogEntry: [%d] %s - %s", entry.Id, entry.Timestamp, entry.Message)
35 }
36}4. Simulating the Client-Server Interaction
Let’s illustrate the request and response simulation:
sequenceDiagram
participant Client
participant Server
Client->>Server: FetchLogs({filter:"all"})
Server-->>Client: LogEntry #1
Note over Server,Client: (1 detik kemudian)
Server-->>Client: LogEntry #2
Note over Server,Client: (1 detik kemudian)
Server-->>Client: LogEntry #3
Server-->>Client: [EOF]
Challenges and Best Practices
1. Error Handling:
A stream can break due to the network or application errors. Make sure error propagation is clear on both sides; use the gRPC status codes (codes.Canceled, codes.Unavailable, etc.).
2. Deadline/Timeout:
The client must set a deadline so the stream doesn’t hang indefinitely.
3. Scalability:
Streaming is well suited for large data volumes, but keep an eye on buffer limits, the goroutine pool, and flow-control settings.
4. Versioning:
Maintain backward compatibility in your stream messages, especially when clients and services span multiple versions.
Conclusion
Server-side Streaming RPC is a powerful gRPC feature that’s ideal for sending large amounts of data or when the response can’t be delivered all at once (for example, progress reports, large query results, update notifications, and so on).
By understanding the server-side streaming implementation pattern, engineers can:
- Save resources (since the data isn’t held as one large batch)
- Improve client responsiveness (data can be processed as soon as part of it arrives)
- Make more optimal use of the HTTP/2 communication channel
If you want to be better prepared for real-time or big-data requirements in your microservices backend, server-side streaming is a must-have skill to master.
References:
If you found this article helpful, don’t forget to share it and give it a star!
Let’s discuss: what kind of streaming RPC implementations have you built—and what were the challenges? Share them in the comments! 🚀