advanced25 min studyLesson 208 in Engineering

Closing one sending direction can leave useful work in the other

Trace streaming message boundaries, request half-close, final response status, backpressure, and cleanup without confusing EOF with cancellation.

On this page

What you’ll understand

  • Distinguish ending request messages from canceling an entire RPC.
  • Count framed messages independently of transport reads and HTTP frames.
  • Explain how streaming flow control and read/write ordering can create deadlock.

A client can finish sending a stream of measurements and still need the server's final result. Ending the request direction therefore means something different from abandoning the call. Streaming APIs expose this distinction through half-close or equivalent completion operations, but naming varies by runtime. Draw both directions and their message states before writing a loop that treats every EOF, write completion, or cancellation as the same event.

Messages, stream directions, and final status have separate boundaries

In gRPC over HTTP/2, each application message has a one-byte compression flag and four-byte message length before its message data. The length describes the encoded message data carried in that message, including compression when selected; decompressed size needs a separate bound. These message boundaries need not align with DATA frames or network reads. A streaming decoder retains partial headers and bodies just like any incremental parser. Receiving one complete message does not mean the request direction has ended, and receiving the end of that direction does not imply that the response direction has delivered final status.

A client's half-close signals that it will send no more request messages. The server can continue reading already sent data and can send responses or a final result, depending on the RPC's declared streaming shape. Cancellation instead asks to terminate the call and stop useful work; it can still race with side effects. A server's final status ends the RPC's response lifecycle, while a transport error can prevent normal status delivery. Keep these events visible in logs and ownership rules so resource cleanup follows the actual API contract rather than assuming that a completed write means the remote application consumed it.

Bmessages=∑i=1n(5+Li)B_{\rm messages}=\sum_{i=1}^{n}(5+L_i)
For gRPC's message envelope, each encoded message contributes five prefix bytes plus L_i encoded data bytes. The sum is message-framing bytes, excluding HTTP/2 frames, headers, trailers, TLS, and transport overhead; compression changes L_i.↔ Scroll wide equations horizontally.

Backpressure couples production to consumption

Flow control prevents a sender from indefinitely overrunning a receiver's available capacity, but it does not automatically bound every application queue before the transport. An application that enqueues messages faster than the RPC writer accepts them can retain unbounded memory locally unless it imposes its own limit. A successful asynchronous write may mean the framework accepted work, not that the peer processed or durably stored it. Document when a buffer can be reused, when cancellation releases ownership, and whether multiple simultaneous reads or writes are permitted by the chosen runtime's streaming API.

Two endpoints can deadlock if both synchronously write enough to exhaust receive credit while neither reads. Each waits for permission that the other would create only by consuming data. The remedy is a protocol and execution design that allows required reading, bounded production, and control progress, not a promise that larger buffers will handle every stream. Some methods intentionally send all input before producing output, while others require concurrent bidirectional progress. Establish that sequencing contract explicitly. Include cancellation and deadlines so a failed peer or broken application dependency cannot retain the stream and its buffers indefinitely.

Bqueued≈Npending L‾retainedB_{\rm queued}\approx N_{\rm pending}\,\overline{L}_{\rm retained}
This application-buffer estimate multiplies pending message count by mean retained bytes per message. It excludes transport buffers and overhead; both a count bound and an appropriate byte bound may be needed when message sizes vary.↔ Scroll wide equations horizontally.

Test the ending paths, not only a successful stream

Exercise an empty input stream, a half-close after several messages, a server response before client completion where the method permits it, cancellation during a partial message, and transport failure before final status. Verify which messages are considered accepted, which business effects need reconciliation, and which resources are released. A count of sent messages is not an acknowledgment protocol; add sequence or operation identifiers if the application needs restartable delivery evidence. Retrying an entire interrupted stream may replay earlier effects unless the method's semantics or a deduplication mechanism explicitly makes that safe.

The queue lab illustrates consumption pressure rather than directional state. For each fresh run Reset, explicitly set BOTH rates, predict, then Play or Step: compare (λ,μ)=(4,3) with (2,3). Both start with four units at t=0; one grows and the other drains by four seconds. Rate edits retain elapsed t and recompute from four, so they do not model a half-close event midway through a run. There are no message envelopes, two-way states, cancellation, or final statuses. Use the worked message ledger to count bytes, and a separate state trace to decide which direction remains open.

Half-close requests and continue receiving a result

A fictional client-streaming gRPC method sends three uncompressed request messages with encoded data sizes 100, 200, and 50 bytes, then half-closes. The server subsequently returns one 40-byte encoded result and final status. Ignore metadata and transport overhead.

  1. The request message-framing total is (5+100)+(5+200)+(5+50)=365 bytes. This is independent of how HTTP/2 groups the bytes into DATA frames.

  2. Half-close means no more request messages will be sent; it does not cancel the pending server result. The response direction still needs to be read to completion.

  3. The single response message occupies 5+40=45 message-framing bytes. Final gRPC status is separate metadata and is excluded from that count; the client needs its outcome, not merely receipt of the 45 bytes.

The result

The client sent 365 framed bytes and later received a 45-byte framed result. Request half-close and successful final RPC completion remained separate events.

Explore the model

Interactive lab

Interactive lab · queue

A producer still needs a consuming peer

For EACH run Reset, set BOTH λ and μ, predict, then Play or Step. Compare (4,3) growth with (2,3) draining from four at t=0 to zero at t=4 seconds. Edits retain t and recompute from four. This single fluid queue has no bidirectional state, half-close, message parser, deadlock detector, or gRPC completion status.

A producer still needs a consuming peerFluid backlog starts at 4 and follows max(0, 4 + (λ − μ)t). The separate M/M/1 residence-time formula assumes Poisson arrivals, exponential service and λ < μ. Current readings: Fluid backlog: 4; Net rate · λ − μ: −1 /s; M/M/1 mean residence: 1 s. All parameters are adjustable with the labeled controls below.02.557.51001.534.56Elapsed time · sFluid backlogλ < μ
Fluid backlogCurrent time
Fluid backlog
4
Net rate · λ − μ
−1 /s
M/M/1 mean residence
1 s
t = 0 s

Fluid backlog starts at 4 and follows max(0, 4 + (λ − μ)t). The separate M/M/1 residence-time formula assumes Poisson arrivals, exponential service and λ < μ. Parameter changes pause playback and recompute the model at the current elapsed time. Reset restores both parameters and returns time to zero. Playback starts paused. Adjust either parameter, then choose Play or Step.

Look forBounded streaming needs consumption and ownership rules; a queue diagram cannot certify that either protocol direction has ended.

Check your understanding

A client half-closes a client-streaming RPC after its final request message. What should it normally still expect under a method that returns a final response?

Try it yourself

Two uncompressed gRPC messages have encoded payload sizes 12 and 18 bytes. Calculate their message-framing total. If they arrive in one DATA frame, how many application messages should a correct decoder emit?

Take a moment to make a prediction or work it out on paper. Open a hint when you need one.

Give me a hint

Count a five-byte prefix per message; transport grouping does not change the declared message boundaries.

Compare with the solution

The total is (5+12)+(5+18)=40 bytes before HTTP/2 and other overhead. A correct decoder emits two application messages once the complete framed bytes are available, even though they share one DATA frame. Final RPC status and direction completion remain separate from that message count.

Take it with you

  • Message framing, request half-close, and final RPC status are different boundaries.
  • Backpressure must extend to application queues and preserve required read progress.
  • Interrupted streams need explicit reconciliation and replay semantics.

Read further

Original sources and deeper explanations to follow at your own pace.

Optional progress, saved in this browser when available. Revisit any lesson at any time.

Lesson not marked complete.