gRPC Streaming Patterns
Streaming lets one call carry many messages over time. The four communication types show the shapes. Patterns show how real teams use those shapes to solve everyday problems such as live feeds, uploads, and chat.
Pattern 1: Live Feed (Server Streaming)
The client subscribes once. The server pushes updates as they happen. Stock prices, match scores, and order tracking use this pattern.
Client ──Watch(AAPL)──▶ Server
Client ◀──price 190.1── Server
Client ◀──price 190.3── Server
Client ◀──price 190.2── Server
func (s *server) WatchPrices(req *pb.WatchRequest, stream pb.PriceService_WatchPricesServer) error {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
select {
case <-stream.Context().Done():
return stream.Context().Err()
case <-ticker.C:
if err := stream.Send(&pb.Price{Symbol: req.Symbol, Value: currentPrice(req.Symbol)}); err != nil {
return err
}
}
}
}
Pattern 2: Chunked Upload (Client Streaming)
Large files do not fit in one message, because receivers cap message size at 4 MB by default. The client slices the file into chunks and streams them. The server replies once at the end.
Client ──chunk 1──▶
Client ──chunk 2──▶ Server assembles the file
Client ──chunk 3──▶
Client ◀──"Upload complete, 3 chunks"── Server
Choose chunks between 32 KB and 1 MB. Smaller chunks add overhead. Larger chunks strain memory.
Pattern 3: Chat (Bidirectional Streaming)
Both sides send whenever they want. Each direction works independently, like two people on a phone call who can speak at the same time.
Alice ──"Hi"──────▶ Server ──"Hi"──────▶ Bob
Alice ◀──"Hello"── Server ◀──"Hello"──── Bob
Pattern 4: Heartbeat
Long streams can look alive while the network silently dies. Send a small heartbeat message every few seconds. The receiver treats a long silence as a broken connection and reconnects.
Pattern 5: Resume with a Cursor
Streams break. Mobile users walk into elevators. Include a cursor or sequence number in each message. The client remembers the last cursor and sends it when it reconnects, so the server resumes where the stream stopped.
message WatchRequest {
string topic = 1;
int64 last_seen_sequence = 2;
}
message Event {
int64 sequence = 1;
bytes payload = 2;
}
Flow Control and Backpressure
A fast sender can overwhelm a slow receiver. HTTP/2 solves this with flow control windows. The receiver grants a limited amount of buffer space, and the sender pauses when the window fills. The send call blocks, which slows the producer naturally.
Fast Server ══▶ [buffer filling] ══▶ Slow Client
│
window full → Server Send() waits
client reads → window opens → Server continues
Respect this mechanism. Never pile unlimited messages into memory queues to dodge blocking.
Keepalive
Keepalive pings test idle connections. Configure sensible intervals so that proxies and firewalls do not close quiet streams.
Choosing a Pattern
| Need | Pattern |
|---|---|
| Subscribe to updates | Server streaming with a cursor |
| Upload a large file | Client streaming in chunks |
| Interactive conversation | Bidirectional streaming |
| Detect dead connections | Heartbeat or keepalive |
| Survive disconnects | Cursor based resume |
Ending a Stream Cleanly
Every stream needs a clear ending. A server finishes a stream by returning from the handler. A client signals the end of its messages by closing the send side. Both sides check the final status code. A missing end signal leaves connections open and wastes memory on both machines.
Client sends: msg, msg, msg, CloseSend
Server reads: msg, msg, msg, EOF
Server returns: final reply + status OK
Message Size Limits
Receivers reject any single message above the configured limit, which defaults to 4 MB. Raise the limit only when the design demands it. Splitting large payloads into many small messages keeps memory use predictable and plays well with flow control.
Summary
Streaming patterns turn the four call types into practical designs: feeds, uploads, chat, heartbeats, and resumable subscriptions. Watch the context, honor flow control, and design every stream to survive disconnects.
