Skip to content
Santekno.com | Tech Tutorials and Trends
EN
📖 0%
02 Jul 2025 · 6 min read ·Article 24 / 110
Go

24 Implementing Bidirectional Streaming RPC

IH
Ihsan Arif
Writer at Santekno · Backend Engineer

Bidirectional Streaming RPC is one of the most powerful features in gRPC, the open-source framework created by Google for service-to-service communication. In modern applications, we often need real-time two-way communication—for example in chat applications, chunked file transfers, monitoring, or even collaborative editing. In this article we will dive into how Bidirectional Streaming RPC is implemented in gRPC using Go—complete with code examples, a simulation, and flow diagrams to clarify how this architecture works.


What Is Bidirectional Streaming in gRPC?

Bidirectional streaming, or duplex streaming, means that both the client and the server can send many messages simultaneously and independently for as long as the connection stays open. This is clearly more flexible than unary RPC (1:1 request-response), server streaming (1:N), or client streaming (N:1).

A simple illustration of the data flow in Bidirectional Streaming:

MERMAID
flowchart LR
    Client -- Pesan 1, 2, 3 --> Server
    Server -- Respon A, B, C --> Client
    Client -- Pesan 4      --> Server
    Server -- Respon D     --> Client

In the diagram above, there is no strict rule about who must send a message first. Both sides can exchange data at the same time.


When Do You Need Bidirectional Streaming RPC?

Bidirectional streaming is useful in many use cases, such as:

  • Real-time Chat: The client and server exchange messages asynchronously.
  • Data Synchronization: For example, collaborative editing of a document.
  • Telemetry/Metrics Monitoring: The client streams metric data, and the server can stream feedback right back.

Example Use Case: A Simple Chat System

To make things easy to follow, we will implement a simple chat system using gRPC Bidirectional Streaming in Go.


1. Defining the gRPC Protocol in .proto

proto
 1// chat.proto
 2syntax = "proto3";
 3
 4package chat;
 5
 6service ChatService {
 7  rpc Chat(stream ChatMessage) returns (stream ChatMessage);
 8}
 9
10message ChatMessage {
11  string user = 1;
12  string message = 2;
13  int64 timestamp = 3;
14}

Here:

  • ChatService has a Chat method that is bidirectional streaming.
  • ChatMessage represents the chat message format.

2. Generate the gRPC Code

Run the following to generate the Go code:

bash
1protoc --go_out=. --go-grpc_out=. chat.proto


3. Server Implementation

File server.go:

go
 1package main
 2
 3import (
 4    "log"
 5    "net"
 6    "sync"
 7
 8    pb "yourrepo/chat"
 9    "google.golang.org/grpc"
10)
11
12type chatServer struct {
13    pb.UnimplementedChatServiceServer
14    mu      sync.Mutex
15    streams map[string]pb.ChatService_ChatServer
16}
17
18func newServer() *chatServer {
19    return &chatServer{
20        streams: make(map[string]pb.ChatService_ChatServer),
21    }
22}
23
24func (s *chatServer) Chat(stream pb.ChatService_ChatServer) error {
25    var user string
26    for {
27        msg, err := stream.Recv()
28        if err != nil {
29            log.Printf("user %s left: %v", user, err)
30            s.mu.Lock()
31            delete(s.streams, user)
32            s.mu.Unlock()
33            return err
34        }
35        user = msg.User
36
37        // Register stream
38        s.mu.Lock()
39        s.streams[user] = stream
40        s.mu.Unlock()
41
42        // Broadcast to everyone (simulate group chat)
43        s.mu.Lock()
44        for u, sstr := range s.streams {
45            if u != user {
46                sstr.Send(msg)
47            }
48        }
49        s.mu.Unlock()
50    }
51}
52
53func main() {
54    lis, err := net.Listen("tcp", ":50051")
55    if err != nil {
56        log.Fatalf("failed to listen: %v", err)
57    }
58    s := grpc.NewServer()
59    pb.RegisterChatServiceServer(s, newServer())
60    log.Println("Chat server running on :50051")
61    if err := s.Serve(lis); err != nil {
62        log.Fatalf("failed to serve: %v", err)
63    }
64}

Explanation:

  • Every user that connects is registered in the streams map.
  • When a message is received, the server broadcasts it to every other client.
  • The streaming connection stays open for as long as the client is connected.

4. Client Implementation

File client.go:

go
 1package main
 2
 3import (
 4    "bufio"
 5    "context"
 6    "log"
 7    "os"
 8    "time"
 9
10    pb "yourrepo/chat"
11    "google.golang.org/grpc"
12)
13
14func main() {
15    conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
16    if err != nil { log.Fatalf("fail to dial: %v", err) }
17    defer conn.Close()
18
19    client := pb.NewChatServiceClient(conn)
20    stream, err := client.Chat(context.Background())
21    if err != nil { log.Fatalf("fail to open stream: %v", err) }
22
23    // Goroutine to receive messages
24    go func() {
25        for {
26            resp, err := stream.Recv()
27            if err != nil {
28                log.Printf("Disconnected: %v", err)
29                return
30            }
31            log.Printf("%s: %s", resp.User, resp.Message)
32        }
33    }()
34
35    // Send messages from stdin
36    reader := bufio.NewReader(os.Stdin)
37    user := os.Getenv("USER")
38    for {
39        text, _ := reader.ReadString('\n')
40        stream.Send(&pb.ChatMessage{
41            User:      user,
42            Message:   text,
43            Timestamp: time.Now().Unix(),
44        })
45    }
46}

Workflow Simulation

Let’s say there are two users: Alice and Bob.

StepAlice (client1)ServerBob (client2)
1Connect & streamRegister stream
2Connect & stream
3“Hi Bob!”Receive, broadcastReceive broadcast
4“Hi Alice!”
5Receive, broadcastReceive broadcast


Sequence Diagram

MERMAID
sequenceDiagram
Alice->>Server: Open stream
Bob->>Server: Open stream
Alice->>Server: Send "Hai Bob!"
Server->>Bob: Send "Hai Bob!" (broadcast)
Bob->>Server: Send "Hai Alice!"
Server->>Alice: Send "Hai Alice!" (broadcast)

Table: RPC Types in gRPC

RPC TypeClient RequestServer ResponseWhen to Use
Unary RPCSingleSingleCRUD, standard APIs
Server StreamingSingleMultiple (stream)Notifications, file downloads
Client StreamingMultipleSingleChunked file upload, batch insert
Bidirectional StreamingMultipleMultiple (stream)Chat, collaborative, telemetry

Challenges & Best Practices

Challenges

  • State management on the server is more involved—you need to map between users and streams.
  • You must handle errors and dropped connections carefully to avoid memory leaks.
  • Data synchronization becomes necessary if you need persistence.

Best Practices

  • Use a mutex/lock when accessing the stream map for thread safety.
  • Close the stream when a client disconnects to avoid resource leaks.
  • Don’t send too many large messages or send too frequently without flow control.
  • Logging is important for monitoring traffic/latency.

Conclusion

Implementing Bidirectional Streaming RPC in gRPC makes applications that need real-time two-way communication far easier to build. With a network layer that is already optimized and a declarative interface via proto files, developers ‘only’ need to focus on the application logic.

And don’t forget: this bidirectional streaming feature must be implemented with proper attention to resource management and concurrency so that your application stays scalable and robust.

Danger
Ready to build applications with a real-time, scalable, and efficient interactive experience? It’s time to explore the potential of gRPC Bidirectional Streaming more deeply in your next project! 🚀

The full source code and demo can be found in this GitHub repo .

Thanks for reading. If you have any questions or suggestions, feel free to leave a comment or DM me on Medium!

Related Articles

💬 Comments