skip to content

Why can socket.socket.recv return only part of the message the peer sent, and what fixes it?

level: middleimportance: must knowfreq 62%

answer

  1. TCP promises order, not boundaries
  2. The request may arrive in pieces
  3. recv takes a maximum, not an exact count
  4. Empty bytes means the peer closed
  5. Length prefix or delimiter, then loop

basics

~20 s

TCP is a byte stream with no message boundaries, so recv returns whatever has arrived, up to the size you asked for. Frame messages yourself with a length prefix or a delimiter, and loop until one message is complete.

solid answer

~50 s

`recv(n)` returns **up to** `n` bytes — at least one, once any data arrives — and it has no idea what a "message" is. TCP guarantees order and integrity, not boundaries: one `sendall` can arrive split across several `recv` calls, and two `sendall` calls can arrive coalesced into one. A return of `b""` is special: the peer closed its side, so it means end of stream, never "nothing yet". The fix is **framing** in your own protocol — either a fixed-size length header, where you read exactly four bytes, decode the length and then read exactly that many, or a delimiter such as a newline, with a buffer holding the partial tail between reads. Both need a loop that keeps calling `recv` until it has the bytes it needs and raises if the stream ends mid-message. `sock.makefile("rb")` supplies that buffering for delimiter protocols.

code

python · 20 lines
python
import socket
import struct

def recv_exactly(sock: socket.socket, n: int) -> bytes:
    buf = bytearray()
    while len(buf) < n:
        chunk = sock.recv(n - len(buf))
        if not chunk:
            raise ConnectionError("peer closed mid-message")
        buf += chunk
    return bytes(buf)

a, b = socket.socketpair()
payload = b"employee,hours\n1200,38.5\n"
a.sendall(struct.pack("!I", len(payload)) + payload)

(size,) = struct.unpack("!I", recv_exactly(b, 4))
print(size, recv_exactly(b, size))
a.close()
b.close()

go deeper

for a junior

Recall the headline fact: TCP is a stream of bytes with no message boundaries, so recv can return part of a message or several at once. Know that empty bytes means the other side closed.

for a middle

Explain the two framing designs — a fixed-size length header or a delimiter with a leftover buffer — and be able to write the loop that reads exactly n bytes and raises when the stream ends mid-message.

for a senior

Demonstrate the hardening: bound the declared length before allocating, cap the delimiter buffer, use explicit network byte order, and handle a mid-message close as a defined error path rather than an exception that kills the worker.

for a principal

Decide whether hand-rolled framing belongs in the system at all, versus an existing message-oriented protocol, and set the standard for how message boundaries, size limits and partial-message failures are specified once for every service.

**The one fact that explains everything.** A `SOCK_STREAM` socket delivers a byte stream. TCP promises that the bytes arrive in order, without gaps or corruption, and that is all it promises. It does not preserve the boundaries of your writes, because segmentation is the kernel's business: your payload may be split across packets by the path's MTU, or several small writes may be coalesced into one segment by Nagle's algorithm, or the receiving kernel may hand you only the part that has arrived when you happen to call `recv`. So `recv(4096)` means "give me at most 4096 bytes, block until at least one is available". Consider an importer streaming payroll rows across a link: `sendall(row_a)` followed by `sendall(row_b)` can surface as one `recv` returning both rows, or as three `recv` calls returning a fragment of the first, the rest of the first plus half the second, and finally the tail. Every one of those is a correct TCP implementation. Code written as `data = sock.recv(4096)` and then "parse `data` as a row" is not a working protocol; it is a program that works on loopback and fails under load or across a real network, which is what makes this such a common interview question. **The three return cases.** `recv` returns non-empty `bytes` when data is available; it returns `b""` — falsy, length zero — when the peer has performed an orderly close or shutdown of its write side, meaning end of stream and no more data ever; and it raises when something goes wrong, such as `ConnectionResetError` if the peer sent an RST or `TimeoutError` if a timeout was set. Confusing the empty-bytes case with "no data right now" produces the classic hot loop that spins at 100% CPU on a closed connection. **Framing option one: a length prefix.** The receiver reads a fixed-size header, decodes the body length, then reads exactly that many bytes. It needs one helper: ```python def recv_exactly(sock, n): buf = bytearray() while len(buf) < n: chunk = sock.recv(n - len(buf)) if not chunk: raise ConnectionError("peer closed mid-message") buf += chunk return bytes(buf) ``` Encode the header with `struct.pack("!I", len(payload))` or `len(payload).to_bytes(4, "big")` — the explicit big-endian byte order matters, because a header written with the machine's native order will be misread by a peer on a different architecture. Always bound the decoded length before allocating: a hostile or buggy peer that claims a four-gigabyte body should be rejected, not obeyed. **Framing option two: a delimiter.** Newline-terminated records are the other standard approach and are what many line protocols use. The receiver keeps a buffer, appends each `recv` result, and splits off complete records while a delimiter is present, leaving the partial tail in the buffer for next time. The subtleties are that the delimiter must not appear inside a payload — so payloads need escaping or encoding — and that the buffer must be capped, since a peer that never sends a delimiter would otherwise grow it without limit. **The buffered shortcut.** `sock.makefile("rb")` wraps the socket in a `BufferedReader`, giving `read(n)`, which returns exactly `n` bytes unless the stream ends, plus `readline()` and iteration. It handles the buffering for a delimiter protocol and is a good fit for request/response servers. Two cautions: the file object and the socket share the descriptor, so close them in a defined order, and a socket with a timeout set does not mix well with the buffered reader because a partially-consumed read that times out can lose buffered data. **Performance detail.** `recv` allocates a new `bytes` object per call. For high-throughput parsing, `sock.recv_into(memoryview(buffer))` reads directly into a pre-allocated `bytearray` and returns the count, avoiding the allocation and letting you parse in place. **How this pairs with the write side.** Framing is a property of the protocol, not of one direction. If your sender assembles the header and body into a single `bytes` and calls `sendall` once, and your receiver reads the header then reads exactly the body, the two agree — regardless of how the kernel chops the traffic in between. If the sender emits fields with several small writes and the receiver assumes each `recv` is one field, they only appear to agree until the network gets interesting.

  • What does it mean when socket.socket.recv returns b"" on a connected TCP socket?
    The peer has closed its side of the connection, or called `shutdown(socket.SHUT_WR)` on it, so no further data will ever arrive — end of stream. It never means "no data right now": a blocking `recv` waits for data, and a non-blocking one raises `BlockingIOError` instead. Treating empty bytes as a transient condition produces a loop that spins on a dead connection, so the correct reaction is to stop reading and close.
  • Why do length headers on the wire use an explicit byte order such as struct.pack("!I", n)?
    `!` selects network byte order, big-endian, and a fixed size with no padding. Without an explicit format character, `struct` uses the machine's native order and alignment, so a header written on a little-endian host is misread by a peer that assumes otherwise — a bug that hides completely while both ends run on the same architecture. `int.to_bytes(4, "big")` and `int.from_bytes(data, "big")` are the equivalent without struct.
  • How do you protect a length-prefixed reader from a peer that declares a huge body?
    Validate the decoded length against a protocol maximum before allocating or reading anything, and close the connection when it is exceeded. Without that check, a single four-byte header can make the process attempt an enormous allocation, which is a trivially cheap denial of service. The same reasoning applies to delimiter framing: cap the buffer so a peer that never sends the delimiter cannot grow it without limit.

TCP is a conveyor belt of letters, not a stack of envelopes: the letters arrive in the right order, but you have to write the address of each envelope's end yourself before you can tell where one message stops.

saying these in an interview costs you the question

  • Assumes one recv returns exactly one message
  • Thinks each send maps to one recv on the peer
  • Reads b"" as "no data available yet"
  • Parses a message without checking it is complete
  • Trusts a declared length header without bounding it
  • Uses native struct byte order for wire headers

context