Chapter 7: The Four Communication Patterns

gRPC supports four distinct RPC patterns. This is one of its biggest advantages over REST (which only has request-response). Understanding when to use each pattern is crucial.

Overview

Pattern 1: UNARY (most common) Client ──── Request ────► Server Client ◄─── Response ──── Server Pattern 2: SERVER STREAMING Client ──── Request ────► Server Client ◄─── Response 1 ── Server Client ◄─── Response 2 ── Server Client ◄─── Response 3 ── Server Client ◄─── (end) ─────── Server Pattern 3: CLIENT STREAMING Client ──── Request 1 ──► Server Client ──── Request 2 ──► Server Client ──── Request 3 ──► Server Client ──── (end) ──────► Server Client ◄─── Response ──── Server Pattern 4: BIDIRECTIONAL STREAMING Client ──── Request 1 ──► Server Client ◄─── Response 1 ── Server Client ──── Request 2 ──► Server Client ──── Request 3 ──► Server Client ◄─── Response 2 ── Server Client ◄─── Response 3 ── Server (independent streams — no required ordering)

Pattern 1: Unary RPC

The simplest pattern. One request, one response. Like a normal function call.

// Proto definition
rpc GetUser(GetUserRequest) returns (User);
Client Server │ │ │─── HEADERS (POST /pkg.Svc/GetUser) ────►│ │─── DATA (GetUserRequest) ──────────────►│ │ [END_STREAM] │ │ │ ← Server processes request │◄── HEADERS (:status 200) ───────────────│ │◄── DATA (User) ────────────────────────│ │◄── TRAILERS (grpc-status: 0) ──────────│ │ │ Timeline: ~1-100ms typical

When to Use Unary

C++ Server Example

Status GetUser(ServerContext* context,
              const GetUserRequest* request,
              User* response) {
  // Look up user
  auto user = db_->FindUser(request->user_id());
  if (!user) {
    return Status(StatusCode::NOT_FOUND, "User not found");
  }
  *response = *user;
  return Status::OK;
}

Pattern 2: Server Streaming

Client sends one request, server sends back a stream of responses. The server decides when to stop.

// Proto definition
rpc ListUsers(ListUsersRequest) returns (stream User);
rpc Subscribe(SubscribeRequest) returns (stream Event);
Client Server │ │ │─── HEADERS + DATA (ListUsersRequest) ──►│ │ [END_STREAM] │ │ │ │◄── HEADERS (:status 200) ───────────────│ │◄── DATA (User 1) ──────────────────────│ │◄── DATA (User 2) ──────────────────────│ │◄── DATA (User 3) ──────────────────────│ │ ... (could be thousands) │ │◄── DATA (User N) ──────────────────────│ │◄── TRAILERS (grpc-status: 0) ──────────│ │ │ Timeline: seconds to hours (long-lived subscriptions)

When to Use Server Streaming

💡 Telecom Use Case

Think of gNMI telemetry streaming. A network device streams interface counters every second to a collector. That's server streaming: one Subscribe request, continuous stream of TelemetryUpdate responses.

C++ Server Example

Status ListUsers(ServerContext* context,
                const ListUsersRequest* request,
                ServerWriter<User>* writer) {
  auto users = db_->GetAllUsers(request->filter());
  for (const auto& user : users) {
    // Check if client cancelled
    if (context->IsCancelled()) {
      return Status(StatusCode::CANCELLED, "Client cancelled");
    }
    writer->Write(user);
  }
  return Status::OK;
}

Pattern 3: Client Streaming

Client sends a stream of messages, server responds with a single message after receiving all of them.

// Proto definition
rpc UploadFile(stream FileChunk) returns (UploadStatus);
rpc RecordRoute(stream Point) returns (RouteSummary);
Client Server │ │ │─── HEADERS (POST /pkg.Svc/UploadFile) ─►│ │─── DATA (FileChunk 1) ────────────────►│ │─── DATA (FileChunk 2) ────────────────►│ │─── DATA (FileChunk 3) ────────────────►│ │ [END_STREAM] │ │ │ ← Server processes all chunks │◄── HEADERS (:status 200) ───────────────│ │◄── DATA (UploadStatus) ────────────────│ │◄── TRAILERS (grpc-status: 0) ──────────│ │ │

When to Use Client Streaming

C++ Server Example

Status UploadFile(ServerContext* context,
                 ServerReader<FileChunk>* reader,
                 UploadStatus* response) {
  FileChunk chunk;
  int64_t total_bytes = 0;
  
  while (reader->Read(&chunk)) {
    // Process each chunk (write to disk, etc.)
    total_bytes += chunk.data().size();
  }
  
  response->set_bytes_received(total_bytes);
  response->set_status("OK");
  return Status::OK;
}

Pattern 4: Bidirectional Streaming

Both client and server send streams of messages independently. The two streams are independent — the server doesn't have to wait for all client messages before responding.

// Proto definition
rpc Chat(stream ChatMessage) returns (stream ChatMessage);
rpc RouteGuide(stream Point) returns (stream Feature);
Client Server │ │ │─── HEADERS ────────────────────────────►│ │─── DATA (msg 1) ──────────────────────►│ │◄── DATA (response 1) ──────────────────│ ← Server can respond immediately │─── DATA (msg 2) ──────────────────────►│ │─── DATA (msg 3) ──────────────────────►│ │◄── DATA (response 2) ──────────────────│ │◄── DATA (response 3) ──────────────────│ │─── [END_STREAM] ──────────────────────►│ ← Client done │◄── DATA (final response) ──────────────│ │◄── TRAILERS (grpc-status: 0) ──────────│ ← Server done │ │ Note: The streams are INDEPENDENT. Server can send at any time, not just in response to client messages.

When to Use Bidirectional Streaming

C++ Server Example

Status Chat(ServerContext* context,
           ServerReaderWriter<ChatMessage, ChatMessage>* stream) {
  ChatMessage msg;
  while (stream->Read(&msg)) {
    // Echo back with modification (simple example)
    ChatMessage reply;
    reply.set_text("Server received: " + msg.text());
    reply.set_timestamp(GetCurrentTime());
    stream->Write(reply);
  }
  return Status::OK;
}

Choosing the Right Pattern

┌─────────────────────────────────┐ │ How many messages each side? │ └─────────────────────────────────┘ │ ┌───────────────┼───────────────┐ │ │ Client sends ONE Client sends MANY │ │ ┌───────┴───────┐ ┌──────┴──────┐ │ │ │ │ Server sends Server sends Server sends Server sends ONE MANY ONE MANY │ │ │ │ UNARY SERVER CLIENT BIDIRECTIONAL STREAMING STREAMING STREAMING

Real-World Examples by Pattern

PatternExampleWhy This Pattern
UnaryGet user profileSimple lookup, small response
UnaryCreate orderOne action, one result
Server streamStock price tickerContinuous updates from server
Server streamSearch resultsStream results as found
Client streamFile uploadLarge data sent in chunks
Client streamGPS trackingDevice sends location updates
Bidi streamChat applicationBoth sides send freely
Bidi streamVoice callAudio in both directions
⚠️ Don't Over-Use Streaming

Streaming adds complexity: you need to handle partial failures, backpressure, reconnection, and ordering. If your data fits in a single response (< 4MB default max), use unary. Streaming is for when you genuinely need it — large datasets, real-time updates, or long-lived connections.

Key Takeaways