Framing turns a byte stream into messages: on a connection whose reads never align with message boundaries, what must the reader's framing loop do?
answer
- bytes arrive, messages do not
- one read is not one message
- buffer outlives a single read
- loop until the buffer is short
- slice exactly, keep the remainder
basics
~20 sA stream connection carries bytes, not messages, so the reader keeps a buffer across reads: append each chunk, test whether a complete message is present from its length prefix or delimiter, consume exactly that message, and retain the remainder.
solid answer
~50 sA stream-oriented connection guarantees byte order, not message boundaries, so one read is not guaranteed to be one message: it can return half a message, two messages and a fragment of a third, or a single byte. The framing loop therefore owns a buffer that outlives any one read. On each chunk it appends to the buffer, then loops: is enough buffered to know where the next message ends? With a length prefix that means the fixed prefix width has arrived, then the declared number of body bytes; with a delimiter it means a delimiter has been found. If a whole message is present, slice exactly it, hand it up, and go round again — the leftover may already hold the next one. If not, return and wait. A half message in the buffer is the normal state, not an error.
code
pseudocode · 18 linesbuffer = empty byte sequence
on chunk_arrived(chunk):
append chunk to buffer
loop:
if length(buffer) < PREFIX_WIDTH:
return // cannot even read the length
declared = read_length(buffer, 0, PREFIX_WIDTH)
total = PREFIX_WIDTH + declared
if length(buffer) < total:
return // body still incomplete
message = slice(buffer, PREFIX_WIDTH, total)
deliver(message)
buffer = slice(buffer, total, length(buffer))
on connection_closed():
if length(buffer) > 0:
report truncated_frame(length(buffer))go deeper
Remember the one fact everything rests on: the connection delivers an ordered stream of bytes, and nothing in it marks where one message stops and the next starts.
Explain the loop concretely: a buffer that survives between reads, a check for the header, a check for the whole body, an exact slice, and a repeat so several messages in one chunk all get delivered.
Show the failure modes you have actually hit — stranded messages when the loop drains only once, interleaved frames from an unserialised write path, and a truncated final frame on close — and describe how you test with adversarial chunk splits.
Frame it as a boundary decision: which layer owns framing, whether every service reimplements the loop or inherits one hardened implementation, and what it costs the organisation when each team writes its own.
## Bytes in, messages out A stream-oriented connection offers three guarantees: what you write arrives, it arrives in the order you wrote it, and nothing is silently lost or duplicated while the connection lives. **Message boundaries are not on that list.** The sending side may coalesce several small writes into one segment or split one large write across many; the receiving side hands your code whatever bytes have arrived by the time you ask for them. **Framing** is the convention that puts the boundaries back. It belongs to the wire contract, not to the transport, and both ends must agree on it before any payload can be decoded — a decoder cannot start until something has told it which bytes constitute one message. The two common conventions are a **length prefix** (a fixed-width count ahead of the body) and a **delimiter** (a reserved byte or sequence that terminates the body). A fixed header combining a marker, a format identifier and a length is the same idea with more fields. ## The buffer is the whole design The single structural decision is that the read buffer outlives a read call. Everything else follows: - **Appending is cheap, consuming is exact.** Each arriving chunk is appended; a message is removed only when all of its bytes are present, and exactly its bytes are removed. - **Zero, one or many messages may become available from one chunk.** The loop after each append runs until the buffer is too short to complete another message, not once. - **The leftover is state.** Whatever remains after the last complete message is the beginning of the next one and must survive until more bytes arrive. - **Incompleteness is not an error.** A reader that raises on a short buffer will fail under load, because load is exactly when reads split. ## What one read can hand you | What a single read returns | What the framing loop must do | |---|---| | Fewer bytes than the length prefix | Buffer them; the length of the next message is not yet known | | The prefix plus part of the body | Buffer; the declared length is known, the body is not complete | | Exactly one whole message | Emit it; the buffer is left empty | | Two messages plus a fragment | Emit both, keep the fragment for the next read | | Zero bytes, repeatedly | Nothing — waiting is normal; only a closed connection is terminal | A connection that closes while the buffer still holds a partial message is a **truncated frame**: the last message was never completed and must be discarded rather than decoded, because a decoder fed a prefix of a message will either fail or, worse, succeed on a value that was never sent. ## The loop, step by step 1. Read a chunk from the connection; append it to the buffer. 2. If the buffer holds fewer bytes than the framing header requires, stop and wait. 3. Read the header to learn where this message ends — the declared length, or the offset of the delimiter. 4. If the buffer does not yet hold the whole message, stop and wait. 5. Slice exactly that message out, pass it to the decoder, and remove those bytes from the buffer. 6. Go to step 2 — the remaining bytes may already contain another complete message. The symmetric obligation on the writer is that it emits a complete frame or none of it: a frame written in pieces from several threads without serialising the write path interleaves with another writer's bytes, and the reader has no way to tell. ## What the loop does not decide Two things sit deliberately just outside this loop. A **declared length must be bounded before it is used to size anything** — the limit discipline itself is a separate subject and is not part of the framing convention. And the loop is indifferent to the payload: it hands up an opaque byte range and never inspects it, which is precisely what lets one framing implementation carry several payload formats. ## Why interviewers ask it It is the shortest question that separates an engineer who has written a protocol from one who has only called a client library. The library hid the buffer; the bug reports that follow — a message that arrives 'cut in half' under load, a consumer that works in a test with small payloads and fails at scale, a reader that stalls holding a complete message it never checked for — all come from a loop that assumed one read equals one message.
- The buffer holds a complete message but no further bytes ever arrive. What is wrong with a loop that only checks for messages immediately after a read?Nothing, if the loop after each append runs until the buffer is too short to complete another message. The bug appears when the loop emits at most one message per chunk: a chunk carrying three messages delivers one and strands two, and if the peer is waiting for replies before sending more, both sides stall. Draining fully on every append is what prevents it.
- How would you test a framing loop for the split-read bugs that only appear under load?Feed the same byte sequence through the loop at every possible split: deliver it one byte at a time, then in two pieces at each offset, then as one chunk, and assert the same sequence of messages comes out each time. Also feed two frames concatenated in one chunk, and a truncated final frame followed by a close, which must be reported rather than decoded.
- Why is a framing bug usually worse than a decoding bug?A decoding failure is localised: one message fails and the next one is unaffected. A framing failure shifts the boundary, so every subsequent message is sliced from the wrong offset and the connection produces an unbounded run of garbage or spurious failures. The damage is per connection rather than per message.
Like a ticker tape sliding under a window: you can only see whatever length happens to be exposed, so you keep the tail of the tape until the next printed record is fully visible, then tear off exactly that record.
saying these in an interview costs you the question
- Assumes one read returns exactly one whole message
- Treats a partial message left in the buffer as a protocol error
- Reads the length prefix, then assumes the body arrived whole
- Emits one message per chunk and strands the rest in the buffer
- Believes flushing after each write forces one matching read
- Decodes a truncated final frame instead of discarding it