API · Grpc

gRPC Streaming Patterns

gRPC Streaming Patterns is the work that makes the systems talk. The API is the contract between the producer and the consumer; the contract is what determines whether the.

John Kihiu12 min read

Most API discussions default to request/response because that's what REST offers. gRPC exposes four distinct RPC shapes, and picking the right one for a given problem — rather than defaulting to unary because it's familiar — is where a lot of gRPC's real value over REST shows up. Each mode is declared explicitly in the .proto file with the stream keyword, so the shape of the interaction is part of the contract, not something inferred from how the client happens to call an endpoint.

The four modes, and what each is actually for

Unary — one request, one response — is the REST-equivalent case and the right default for anything that fits a simple request/response model: fetching a record, running a calculation, submitting a form. Server-streaming — one request, a stream of responses — fits a case where a single request implies an ongoing or large series of results: subscribing to price updates, streaming a large query result set page by page instead of materializing it all in memory. Client-streaming — a stream of requests, one final response — fits uploading data incrementally, like a client sending chunks of a large file or a batch of telemetry events and getting one acknowledgment at the end. Bidirectional streaming — both sides stream independently over the same connection — fits genuinely two-way interactive use cases: a chat session, live collaborative editing, or a negotiation protocol where either side can send at any time.

PROTOBUF · ALL FOUR MODES
service TelemetryService {
  // Unary
  rpc GetDeviceStatus (DeviceRequest) returns (DeviceStatus);

  // Server streaming
  rpc WatchDeviceEvents (DeviceRequest) returns (stream Event);

  // Client streaming
  rpc UploadReadings (stream Reading) returns (UploadSummary);

  // Bidirectional streaming
  rpc LiveDiagnostics (stream Command) returns (stream Response);
}

Backpressure becomes your problem, not the framework's

Once a method streams, whoever's consuming the stream has to keep up, or the sender needs to slow down — and that coordination doesn't happen automatically. gRPC implementations expose flow control at the HTTP/2 level, but application code still needs to handle the case where a server is producing events faster than a slow client can consume them: buffering indefinitely leaks memory, dropping silently loses data. Server-streaming and bidi-streaming methods should have an explicit strategy — bounded buffers, backpressure signals, or deliberate rate limiting on the producer side — rather than assuming the stream will always be consumed as fast as it's produced.

Deadlines apply per-call, and streams complicate them

A unary gRPC call typically has a deadline — the client says "fail this if it takes longer than N seconds" — and that model gets awkward for a stream that's supposed to stay open for minutes or hours, like a live subscription. Long-lived streaming RPCs generally shouldn't use a short fixed deadline; instead they rely on the connection's keepalive settings and application-level heartbeats to detect a dead peer, with the deadline reserved for the initial handshake or for a stream that's explicitly meant to be bounded in time.

A stream that never completes still holds server resources

Every open streaming RPC holds a goroutine/thread and buffer state on the server for as long as it's open. A client that opens a server-streaming subscription and never properly closes it (crashes without a clean disconnect, or a mobile app that backgrounds) can leak server-side resources if the server doesn't detect the dead connection via keepalive pings and clean up.

Cancellation has to propagate explicitly

When a client cancels a streaming call — closes the app, navigates away — the server needs to actually notice and stop doing work, not keep streaming into a void. gRPC surfaces context cancellation (Go's context.Context, similar mechanisms in other languages) specifically so a long-running streaming handler can check "is the client still there" periodically and abort expensive work — a database cursor scan, a downstream call — as soon as it's no longer needed. Code that ignores the cancellation signal keeps burning server resources on work nobody's listening for anymore.

Default to unary; reach for streaming when the shape actually calls for it

The most common mistake isn't picking the wrong streaming mode — it's reaching for streaming when unary would do, adding backpressure and cancellation complexity for no real benefit. If a client wants a snapshot, use unary and let it poll or use server-streaming with a subscription pattern only when it genuinely needs push updates. Streaming buys you efficiency and real-time behavior at the cost of connection lifecycle management that a stateless unary call never has to think about.

Wrapping up

gRPC's four RPC modes map cleanly onto four real interaction shapes — request/response, subscribe, upload, and full duplex — but streaming modes trade simplicity for real operational complexity: backpressure, deadline handling, and cancellation propagation all become the application's responsibility. Default to unary, and reach for a streaming mode only when the interaction genuinely doesn't fit request/response.

John Kihiu
Acumatica ERP Developer · Laravel Engineer

Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.