KomuraSoft LLC
Chapter 6

Streams and framing — TCP's send and recv drift apart

What TCP delivers is a flow of bytes, not a sequence of messages. Send boundaries do not survive into recv — if you need boundaries, the application builds its own. Taught through a classic bug.

TCP is working correctly, yet the data "breaks"

Across five chapters we have watched TCP keep its three promises admirably. Yet in practice, defects like "received data occasionally goes missing" or "two messages arrive stuck together" are common on top of TCP. The packet capture shows no retransmissions, no loss. TCP is perfectly healthy. Still the application's data looks broken — why?

The answer lies in what TCP actually delivers. TCP promised "the same byte sequence, in the same order" — not "with the same boundaries." As Chapter 3 showed, TCP's protagonist is the byte, not the segment. Even if you call send() twice — 300 bytes then 200 bytes — they may merge into a single segment on their journey, or be recut into 120 + 380. The boundaries of the chunks that the receiver's recv() returns are unrelated to the sender's send boundaries.

This property is called a byte stream (no preservation of message boundaries). A socket is not a mail carrier delivering a bundle of letters; it is a seamless water pipe. Just as a pipe keeps no boundary between "the first glass of water" and "the second," neither does TCP.

Diagram showing the boundaries of the sender's two send calls failing to match the boundaries of the chunks the receiver's recv returns, so messages appear cut in the middle or concatenated

Send boundaries (top row) are not inherited by recv boundaries (bottom row). Only byte content and order are promised. When '1 send = 1 recv' holds, it is a coincidence.

Two places where boundaries change

Where do boundaries actually change? There are two classic sites.

Merging — small sends get bundled (the Nagle algorithm)
Even 1 byte of data carries about 40 bytes of headers. To avoid trickling out tiny transmissions, while an ACK is outstanding the next small data is held back and sent in one segment. When latency hurts interactive traffic, it can be disabled with TCP_NODELAY
Splitting — large sends get carved up (MSS)
One segment can carry only so much data (MSS: around 1460 bytes in many environments). A 10 KB send is always split into multiple segments and may appear at the receiver across several recv calls

Both are correct optimizations as far as TCP is concerned. In development (local, low latency, small data), '1 send = 1 recv' keeps holding by luck, so bugs built on that assumption fire only in production, only occasionally. It is the birthplace of "unreproducible" defects.

Practice 6-1 — The byte-stream property

'Received data occasionally goes missing or sticks together' — let's uncover the real identity of this classic production bug.

Q1. On a TCP connection, the sender called send() twice (300 bytes, then 200 bytes). Which is a possible outcome of the receiver's recv() calls?

Q2. Code that 'parses whatever recv() returned as one JSON message' works in development but occasionally fails in production. What is the most likely cause?

Q3. When small sends are made in rapid succession, TCP may bundle them into a single segment (the Nagle algorithm). Which correctly states this behavior's purpose and side effect?

Framing — the application builds its own boundaries

If you need message boundaries, the only way is to embed the boundary information inside the byte sequence itself. This is called framing. There are two standards.

The length-prefix scheme
Prepend to each message a fixed-size field (e.g., 4 bytes) saying how many bytes the body is. The receiver accumulates in a buffer until the full length is present, then cuts the message out. Strong for binary; the underpinnings of HTTP/2 and gRPC are in this family
The delimiter scheme
Place a marker such as a newline at the end of each message (one line = one message). Human-readable and easy to debug. But a guarantee that the body never contains the delimiter (escaping or encoding) is mandatory. HTTP/1.1 headers and the Redis protocol are in this family

With either scheme, the receiver's code takes the same shape — append what recv delivered to a buffer, cut out only completed messages, and carry the remainder forward. The loop depends in no way on the number or boundaries of recv calls. Conversely, as long as this loop is correct, no boundary TCP chooses can break the application.

How to remember it: TCP has no concept of 'one message.' Inventing 'one message' is always the application's job. Even when you use an off-the-shelf protocol or library, this framing is at work underneath.

Practice 6-2 — Build your own boundaries (framing)

If boundaries are not preserved, the boundary information has to go inside the data itself.

Q4. In the length-prefix scheme (each message is sent with its body length prepended), which is the correct receiver implementation?

Q5. When using a delimiter such as a newline to mark message boundaries (e.g., one line = one message), what must you always design for?

Q6. Two messages, each a 4-byte length prefix + 300-byte body (304 bytes each), were sent back to back (608 bytes total). The receiver's first recv() returned 500 bytes. How many more bytes must be received to complete the second message?

bytes

Key takeaways from this chapter

  • TCP is a byte stream — the promise is 'the same bytes in the same order.' Send boundaries are not preserved
  • Boundaries change routinely through merging (Nagle) and splitting (MSS). '1 send = 1 recv' is a development-environment coincidence
  • If you need boundaries, use framing — a length prefix or a delimiter. Delimiters demand escaping by design
  • The receiver-side pattern: 'accumulate, cut out only what is complete, carry the rest forward.' Never depend on recv boundaries

All the parts are now on the table. In the final chapter, we triage eight practice-flavored incident cases by translating each into 'which chapter is this about?'