When I was working with HTTP, I carried a quiet assumption in the back of my mind:
if I send one thing, the other side receives one thing.
It felt obvious. Almost too obvious to question.
Working at higher layers trains you to think that way. A request feels like a single unit. A response feels complete. Boundaries between things feel real. Everything appears neat and self-contained, so you stop thinking about what’s underneath. Requests feel atomic. Responses feel whole.
So when I moved from HTTP to TCP, I took that assumption with me.
It didn’t last long.
I skimmed the syntax, wrote a small TCP server, and tried to apply what I already knew. Almost immediately, things started behaving in ways I didn’t expect. I would write data once and receive it back in fragments, like pieces of something that was supposed to be whole. Other times, two separate writes arrived merged together. Sometimes nothing arrived at all until much later.
I thought my code was buggy.
Then it clicked.
I was still treating TCP like HTTP.
With HTTP, TCP is invisible. Frameworks hide the stream. You send one request. You get one response. Errors are explicit. Partial data never leaks through, so you never have to think about it.
Why TCP behaves this way
TCP has one core responsibility: make sure bytes sent from one side arrive at the other side reliably and in the same order.
That’s it.
It doesn’t understand messages. It doesn’t preserve boundaries. It doesn’t care how you intend the data to be interpreted. Once bytes enter the TCP stream, they are just bytes, and they’re delivered whenever the operating system decides they’re available.
That’s why a single write can arrive in multiple data events. It’s also why multiple writes can be merged together. The chunking you see is not a TCP feature it’s a delivery detail.
What you do with those bytes is entirely your responsibility.
Once I accepted that, a few things became unavoidable:
- A
dataevent does not mean a full message has arrived - Message boundaries are something I must define and enforce
- Offsets are not bookkeeping — they are correctness
- Most protocol bugs fail silently, not loudly
If you advance an offset incorrectly by even a single byte, the parser doesn’t always crash. It keeps running — just on corrupted state. That’s how bugs slip through unnoticed.
The consequence most examples ignore
If TCP doesn’t preserve message boundaries, then any protocol built on top of it has to define its own.
Without explicit framing, you can’t tell where one message ends and the next begins. You can’t know whether you’ve received a complete message or just part of one. And if you guess wrong, the failure isn’t always obvious it’s often silent.
This is how protocol bugs happen.
What this realization forces you to do
Once you accept that TCP gives you nothing but an ordered stream of bytes, a few rules become unavoidable.
You need a per-connection buffer, because data can arrive incomplete or merged together. You need explicit framing, because message boundaries don’t exist unless you define them. And most importantly, you must only parse data that is complete.
Parsing incomplete data is not a recoverable mistake.
If you advance an offset incorrectly and continue parsing, the system doesn’t always fail loudly. It keeps running, just on corrupted state.
I learned this the hard way.
If a parse attempt fails due to incomplete data, you must roll back and wait. The offset must return to where parsing started. Anything else silently poisons the protocol.
This is also where state machines become necessary. Different bytes mean different things depending on what stage the connection is in. Without strict state enforcement, even correctly framed data can be interpreted incorrectly.
This is the kind of work TCP pushes onto you whether you’re ready for it or not.
Once I stopped treating TCP like it owed me structure, everything else followed naturally.
Buffers were no longer optional. Framing was no longer a detail. Parsing became something you earn, not something you assume. Every byte had to be accounted for, every offset justified, every failure handled deliberately.
In the next post, I’ll go one level deeper — into what it actually means to build a protocol on top of a byte stream. Not libraries or abstractions, but the mechanics: buffers, framing, and why parsing must be restartable.
This is where TCP stops being confusing and starts being honest.
This post is part of a series where I’m building BitTorrent from scratch in Node.js, starting at raw TCP and working upward. The focus is on correctness and protocol discipline, not shortcuts or abstractions.
If you’ve worked with TCP at this level before, I’d be interested to hear which assumption broke for you first.
Code and experiments live here:
https://github.com/Lastmile04/bittorrent_from_scratch
Top comments (0)