TCP moves bytes. It never agreed to move your messages.
The common read: A socket write is a message send, so three writes are three arrivals and the receiver's read loop sees the messages I sent.
Nothing preserved your boundaries.
TCP is specified as a stream of bytes, and the specification says out loud that your write calls have no standing in it: RFC 9293 guarantees "no correlation between the boundaries of TCP segments sent and received and the boundaries of the read or write buffers of user application data", which is a sentence about your code rather than about the network, and it licenses the stack to merge, split and re-chunk whatever you handed it, anywhere on the path, for any reason, forever.
UDP is the other contract and it is worth naming as an absence rather than as a feature: there is no re-chunking, because there is nothing to re-chunk, a datagram arrives whole or not at all, which is why the three sends TCP fused arrived as three messages on the probe's control run.
So framing is yours, in every TCP protocol you will ever write, and the only real decision is which one you took deliberately: a length prefix, a delimiter, or a self-describing encoding. The version of this that reaches a pager is a hand-rolled protocol whose data handler assumes one read is one message.
It passes every test against localhost, passes staging, and then cuts a message in half the week a proxy lands between the two services, and it arrives as "the payment service is corrupting our JSON", filed against the proxy by an engineer holding a packet capture that shows every byte leaving intact and arriving intact, which is the frustrating part, because the bytes were never wrong.
Take the price along with the fix. A length prefix is an ENCODING decision: it buys parseability and it buys nothing else, it authenticates nothing, and the length you just read is a number an attacker chose, so it is bounded before it is allocated against, or you have traded a framing bug for a memory one.
