Designing a Binary Protocol for Modern APIs
There is something almost suspicious about the modern API.
We spend enormous amounts of time designing endpoints, authentication systems, database schemas, caching layers, observability pipelines, and distributed architectures.
Then, somewhere underneath all of that sophistication, two machines eventually have to do something incredibly simple:
send bytes to each other.
A developer writes:
POST /api/orders
Content-Type: application/json
and thinks they are designing an API.
They are not.
They are designing the human-facing surface of a much deeper communication system.
Underneath the HTTP request is a TCP connection. Underneath TCP is a stream of bytes. Somewhere in that stream, a machine needs to determine where one message ends, what operation it represents, which fields exist, how large those fields are, whether the data is valid, whether the sender understands the protocol version, and what response should come next.
The JSON is merely one possible representation of that conversation.
And this raises an interesting question:
What if we designed the binary protocol underneath the API deliberately, rather than letting serialization happen almost accidentally?
That is the idea behind designing a binary protocol for modern APIs.
Not because JSON is bad.
JSON is excellent.
It is readable, ubiquitous, easy to debug, supported by almost everything, and incredibly productive.
But modern systems increasingly operate under conditions where every byte, allocation, round trip, CPU cycle, and microsecond can matter.
High-frequency trading systems.
IoT networks.
Mobile applications.
Game servers.
Distributed databases.
Edge computing.
Service-to-service communication.
Real-time collaboration.
AI inference infrastructure.
Financial systems.
Embedded devices.
Cloud services processing billions of requests.
In these environments, the protocol itself becomes architecture.
And designing one forces us to confront something software engineers sometimes avoid:
communication is a data structure.
1. An API Is Really a Conversation Between Machines
Let's simplify an API.
Imagine a client wants to retrieve a user.
At the application level:
GET /users/42
At the conceptual level:
Client:
Give me user 42.
Server:
Here is user 42.
At the protocol level, however, this becomes something closer to:
[length]
[version]
[message type]
[request ID]
[payload]
The server needs to decode this structure.
For example:
+--------+---------+----------+------------+---------+
| Length | Version | Msg Type | Request ID | Payload |
+--------+---------+----------+------------+---------+
| 4 B | 1 B | 1 B | 8 B | N bytes |
+--------+---------+----------+------------+---------+
Suddenly, API design becomes a binary data-layout problem.
And this is where things get interesting.
A protocol is not simply a format.
It is a shared agreement about meaning.
If one machine interprets byte 0x01 as "create user" while another interprets it as "delete user," you do not have an API.
You have a very expensive debugging session.
2. Why Build a Binary Protocol?
The obvious question is:
Why not just use JSON?
For many systems, the answer is:
You should.
A binary protocol introduces complexity.
You lose human readability.
You need encoding and decoding logic.
Debugging becomes harder.
Versioning requires more discipline.
You need tooling.
You may need language-specific libraries.
And if you design the protocol badly, you can create something worse than JSON.
So binary protocols should not be treated as an automatic performance upgrade.
The real question is:
What properties does the system need from communication?
Binary protocols can provide:
- smaller messages
- faster serialization
- faster parsing
- predictable memory usage
- lower bandwidth consumption
- fewer allocations
- efficient numeric representation
- explicit message framing
- efficient streaming
- better suitability for constrained devices
- stronger control over wire compatibility
But the important insight is that binary is not synonymous with fast.
A badly designed binary protocol can be slower than JSON.
Imagine a protocol that requires:
allocate buffer
copy buffer
decode header
allocate object
decode field
allocate string
copy string
validate object
convert object
You could end up spending more CPU and memory than a carefully implemented JSON parser.
The real advantage comes from controlling the entire pipeline.
3. Start With the Message, Not the Endpoint
One of the biggest mistakes in API design is beginning with URLs.
Developers think:
/users
/users/{id}
/orders
/orders/{id}
/payments
But a binary protocol has no inherent concept of URLs.
It has messages.
So start there.
For example:
UserRequest
could contain:
operation
user_id
request_id
and a response could contain:
status
user_id
name
email
created_at
Now define those fields precisely.
For example:
UserRequest
field 1:
operation
uint8
field 2:
request_id
uint64
field 3:
user_id
uint64
The wire representation might look conceptually like:
01 00 00 00 00 00 00 00 7B 00 00 00 00 00 00 00 2A
Humans hate this.
Computers love it.
The important part is not the hexadecimal.
The important part is that the meaning of every byte is defined.
4. Framing: Where Does a Message End?
This is one of the first genuinely difficult problems.
Suppose TCP gives you:
101100101011001010101010...
TCP gives you a byte stream.
It does not inherently tell you:
"Here is packet number one."
If your protocol sends:
Message A
Message B
Message C
the receiver might see:
Message A + first half of B
or:
last half of A + Message B + Message C
This is called TCP stream fragmentation/coalescing.
Therefore, your application protocol needs framing.
The simplest approach is a length-prefixed frame:
+------------+-------------------+
| Length | Payload |
| 4 bytes | N bytes |
+------------+-------------------+
If length is:
00000042
the receiver knows the next 42 bytes belong to the message.
Conceptually:
read 4 bytes
parse length
read N bytes
decode message
This sounds trivial.
It isn't.
Because now you have to answer:
What happens if the client says the message is 10 GB?
You cannot simply allocate:
let buffer = vec![0; length];
without validation.
A malicious or malfunctioning client could send:
FF FF FF FF
and turn your protocol into a memory-exhaustion machine.
So framing must include resource limits.
For example:
MAX_FRAME_SIZE = 16 MB
Then:
if length > MAX_FRAME_SIZE:
reject connection
This is one of the recurring themes of protocol design:
Every field is both data and an attack surface.
5. A Better Header
A modern binary protocol usually benefits from a structured header.
For example:
+---------+---------+----------+----------+-------------+
| Magic | Version | Flags | Type | Request ID |
+---------+---------+----------+----------+-------------+
| 2 B | 1 B | 1 B | 2 B | 8 B |
+---------+---------+----------+----------+-------------+
+------------+------------------+
| Payload Len| Header Checksum |
+------------+------------------+
| 4 B | 4 B |
+------------+------------------+
Now every message begins with something predictable.
For example:
Magic = 0xDA7A
Version = 1
Flags = 0
Type = USER_GET
Request ID = 92831
Payload Len = 84
Why have a magic number?
Because it gives the receiver a recognizable signature.
If the server receives:
DA 7A
it knows:
This looks like my protocol.
If it receives:
FF 42
it can reject it.
This becomes particularly useful when debugging raw streams, detecting protocol mismatches, and preventing one protocol from accidentally being interpreted as another.
6. Versioning Is Not Optional
Here is where many protocol designs eventually become painful.
Version one says:
User:
id
name
Then version two arrives:
User:
id
name
email
Then version three:
User:
id
name
email
phone
How does an old client handle a new server?
This is the fundamental problem of protocol evolution.
The naive approach is:
version = 1
version = 2
version = 3
and then writing completely different parsers.
That works for a while.
Then you end up with:
if version == 1:
...
elif version == 2:
...
elif version == 3:
...
elif version == 4:
...
elif version == 5:
...
Congratulations.
You have created protocol archaeology.
A better approach is often forward-compatible field evolution.
For example:
field_id
field_type
field_length
field_value
This lets an older client encounter:
field 7
and simply say:
I don't know what field 7 means.
Skip it.
That one capability can dramatically simplify protocol evolution.
7. TLV: The Beautifully Boring Idea
One useful pattern is Type-Length-Value, commonly abbreviated TLV.
The structure is:
+--------+--------+----------------+
| Type | Length | Value |
+--------+--------+----------------+
For example:
Type = 1
Length = 8
Value = user ID
Then:
Type = 2
Length = 13
Value = username
A message might become:
01 08 [8 bytes]
02 0D [13 bytes]
The advantage is flexibility.
Unknown fields can be skipped.
Fields can be reordered.
New fields can be introduced.
Optional fields do not necessarily require breaking the entire schema.
TLV is not magical.
It introduces overhead.
But sometimes a few bytes of metadata are an excellent trade for compatibility.
This is one of those places where protocol design becomes a negotiation between:
compactness and evolvability.
8. Don't Serialize Everything as Strings
JSON encourages a particular habit:
{
"user_id": "92831",
"active": "true",
"balance": "1500.50"
}
Humans can read it.
Computers now have to interpret strings.
A binary protocol can represent these values directly.
user_id:
uint64
active:
bool
balance:
int64
For money, you might store:
150050
representing:
1500.50
with an agreed scale.
That eliminates floating-point ambiguity and unnecessary string parsing.
Similarly, timestamps can be represented as:
int64
rather than:
"2026-09-07T00:25:43Z"
The latter is much nicer for humans.
The former is much cheaper for machines.
Again, the protocol's job is not to make bytes beautiful.
Its job is to make meaning unambiguous.
9. Endianness: The Tiny Detail That Can Ruin Your Weekend
Suppose you have:
uint32 = 1000
How are those four bytes represented?
One common convention is network byte order, which is big-endian.
The value:
1000
becomes:
00 00 03 E8
If your protocol does not explicitly define byte order, different implementations can disagree.
Your Rust implementation works.
Your C implementation works.
Your JavaScript implementation appears to work.
Then your embedded device sends:
E8 03 00 00
and your server thinks:
3892510720
instead of:
1000
This is why protocol specifications must be painfully explicit.
Define:
- integer sizes
- signedness
- byte order
- string encoding
- floating-point representation
- timestamp format
- maximum lengths
- null representation
- optional field semantics
A protocol specification should leave very little room for interpretation.
10. Strings Are More Complicated Than They Look
Consider:
username = "Derek"
How does the receiver know its length?
One option:
04 44 65 72 65
But if you're using UTF-8, length means bytes, not necessarily characters.
For example:
hello
has:
5 characters
5 bytes
But another Unicode string might have:
5 characters
10 bytes
So a protocol should generally define:
String length is measured in UTF-8 encoded bytes.
Then:
[length: uint32]
[UTF-8 bytes]
The receiver can safely skip the correct number of bytes.
And again:
MAX_STRING_LENGTH
must exist.
Never assume that the other side will send a reasonable amount of data.
11. Null, Empty, Missing, and Zero Are Different
This sounds philosophical.
It isn't.
Imagine a field:
email
These could represent different states:
missing
null
""
"example@example.com"
And:
age = 0
is not necessarily the same as:
age = missing
A binary protocol needs semantics for these states.
For optional fields, you might use a presence bitmap:
presence:
10110100
Each bit indicates whether a field exists.
Or TLV naturally allows absence:
field 1 exists
field 2 missing
field 3 exists
This is especially important for PATCH-like operations.
For example:
UpdateUser
could mean:
name = missing
email = null
phone = "+260..."
Those three states can mean:
don't modify name
clear email
set phone
A good protocol captures these semantics explicitly.
12. Request IDs Turn APIs Into Multiplexed Conversations
One of my favorite ideas in protocol design is the request ID.
Imagine sending:
Request 101
Request 102
Request 103
without waiting for each response.
The server might respond:
Response 102
Response 101
Response 103
This is completely fine if every request has an ID.
For example:
request_id = 101
The client maintains:
101 -> waiting for user
102 -> waiting for order
103 -> waiting for payment
When response 102 arrives:
resolve request 102
Now your connection can support concurrent in-flight operations.
This is fundamentally different from:
request
wait
response
request
wait
response
You have transformed the connection into a multiplexed communication channel.
That idea is incredibly powerful.
It reduces the need to open many connections and allows the network to carry multiple logical conversations simultaneously.
13. Streaming Changes Everything
Traditional APIs often assume:
request -> response
But modern applications frequently need:
request -> response
response
response
response
response
Think:
- AI token streaming
- database change feeds
- logs
- market data
- multiplayer events
- chat
- telemetry
- file transfers
Your protocol therefore needs to distinguish between:
single response
and:
stream response
You could have flags:
FINAL = 0x01
STREAM = 0x02
ERROR = 0x04
COMPRESSED = 0x08
Then:
flags = STREAM
means:
More messages are coming.
And:
flags = STREAM | FINAL
means:
This is the final message.
Now the protocol itself understands streaming.
14. Compression Should Be Negotiated
Suppose a response is:
2 MB
Compression might reduce it to:
200 KB
Fantastic.
But compression costs CPU.
For a tiny message:
200 bytes
compression may make things worse.
So compression should be negotiated.
During connection setup:
Client:
supported compression:
none
gzip
zstd
Server:
Selected:
zstd
Then a flag indicates:
COMPRESSED
This illustrates another important protocol principle:
Capabilities should be negotiated rather than assumed.
The client and server may have different capabilities.
A protocol should allow them to discover a common subset.
15. Authentication Belongs Above the Raw Bytes
A binary protocol does not automatically become secure because it is binary.
This:
01 02 03 04
is not more secure than:
{"user":42}
Security comes from cryptography, authentication, authorization, integrity protection, and correct implementation.
A modern binary API might run over:
TLS
and then use its own protocol inside the encrypted connection.
For example:
TLS
↓
Binary API Protocol
↓
Application Message
You might also have:
authentication token
inside the protocol.
But be careful.
Do not invent your own cryptography.
Use established transport security mechanisms.
The protocol should focus on communication semantics, not pretending to become a cryptographic research project.
16. Error Handling Should Be a First-Class Message
One of the worst API designs is:
something went wrong
followed by a random string.
A binary protocol should define errors explicitly.
For example:
ErrorResponse
code:
uint16
request_id:
uint64
message:
string
details:
optional bytes
Possible codes:
1001 INVALID_REQUEST
1002 UNAUTHORIZED
1003 NOT_FOUND
1004 RATE_LIMITED
1005 INTERNAL_ERROR
1006 UNSUPPORTED_VERSION
Now clients can program against stable error semantics.
Instead of:
if (error.message.includes("not found")) {
you can write:
if (error.code === NOT_FOUND) {
This distinction matters.
Messages are for humans.
Codes are for programs.
17. Error Messages Should Not Leak the System
Suppose the database throws:
PostgreSQL error:
duplicate key value violates unique constraint users_email_key
You probably don't want to send that directly to a client.
Your protocol might expose:
code = EMAIL_ALREADY_EXISTS
while the internal logs contain:
database constraint violation...
This creates a clean separation:
internal implementation
↓
domain error
↓
protocol error
That separation makes systems easier to evolve.
18. Checksums and Integrity
Should every binary protocol have a checksum?
Not necessarily.
If you're using a reliable and integrity-protected transport such as TLS, adding another checksum may be redundant for many API use cases.
But checksums can be useful in environments where corruption detection is important, particularly when operating closer to hardware, unreliable links, storage, or custom transports.
For example:
CRC32(payload)
could be included in a frame.
The receiver calculates:
CRC32(received_payload)
and compares it.
If they differ:
FRAME_CORRUPTED
This is a design decision rather than a universal requirement.
Protocol engineering is full of these trade-offs.
19. The Most Important Field Might Be the Length
There is something wonderfully boring about a length field.
payload_length = 128
It doesn't sound revolutionary.
But it enables:
- framing
- buffer management
- streaming
- skipping unknown fields
- bounds checking
- memory planning
- efficient reads
- partial decoding
Length is one of the fundamental pieces of structure in a byte stream.
But it must be validated.
Never trust it.
The decoder should enforce:
length <= maximum_frame_size
and:
offset + length <= buffer_size
Otherwise, your protocol parser can become a vulnerability.
And binary parsing vulnerabilities are particularly dangerous because the parser sits at the boundary between untrusted bytes and trusted program state.
20. Design the Decoder Before the Encoder
This is a surprisingly useful technique.
Developers often think:
How do I encode my object?
Instead ask:
What must the decoder safely understand?
Suppose you define:
encode(User)
first.
You might create something beautiful.
Then six months later, your decoder receives:
malformed packet
truncated packet
unknown field
future field
huge length
duplicate field
invalid UTF-8
unexpected type
Now the parser becomes a nightmare.
Design the decoding state machine first.
Something like:
READ_HEADER
↓
VALIDATE_MAGIC
↓
VALIDATE_VERSION
↓
VALIDATE_LENGTH
↓
READ_PAYLOAD
↓
DECODE_FIELDS
↓
VALIDATE_SEMANTICS
↓
DISPATCH_MESSAGE
Then design encoding to produce messages that satisfy that grammar.
This mindset leads to more robust protocols.
21. The Decoder Is a Security Boundary
A binary decoder should assume the input is hostile.
Even if you control both client and server.
Because eventually someone will:
- fuzz it
- send corrupted packets
- write another client
- accidentally produce malformed messages
- replay old requests
- send enormous fields
- exploit integer overflow
- trigger parser edge cases
Consider:
length = 0xFFFFFFFF
If you convert this incorrectly:
uint32 -> signed int
you could get:
-1
Then your code starts doing extremely interesting things.
Integer overflow.
Out-of-bounds reads.
Memory exhaustion.
Infinite loops.
Parser desynchronization.
Protocol implementations need defensive programming.
22. Protocol State Machines
A protocol is not only bytes.
It is also state.
For example:
CONNECTED
↓
HELLO
↓
AUTHENTICATING
↓
AUTHENTICATED
↓
READY
↓
CLOSING
Maybe a client cannot send:
GET_USER
before:
AUTH
So:
state = CONNECTED
message = GET_USER
should produce:
PROTOCOL_ERROR
This is essentially a finite-state machine.
And thinking about protocols this way is incredibly useful.
You can ask:
What messages are legal in each state?
For example:
CONNECTED:
HELLO
CLOSE
AUTHENTICATING:
AUTH
CLOSE
READY:
REQUEST
PING
CLOSE
Now protocol correctness becomes something you can test.
23. Heartbeats and Dead Connections
Distributed systems have an annoying property:
A connection can disappear without telling you it disappeared.
The server may think:
client is alive
while the client thinks:
server is dead
Heartbeats help.
The protocol might define:
PING
PONG
Every:
30 seconds
the server sends:
PING
and expects:
PONG
within:
10 seconds
If not:
connection = dead
But heartbeats should be designed carefully.
A system with millions of clients sending heartbeats every second can create a surprisingly large amount of traffic.
Protocol design is often about avoiding small problems multiplied by millions.
24. Idempotency Belongs in the Protocol Conversation
Imagine a payment request:
request_id = 92831
The client sends it.
The network fails.
The client doesn't know whether the server processed it.
So the client retries:
request_id = 92831
The server receives it again.
What happens?
A protocol can use request IDs as part of idempotency semantics.
The server might maintain:
92831 -> completed
and return the previous result rather than processing the operation again.
This is especially important for operations such as:
create payment
charge card
place order
transfer funds
A protocol doesn't automatically solve distributed systems problems.
But a good protocol gives the application enough structure to solve them.
25. Protocol Design Is Distributed Systems Design
This is the deeper point.
Once you design a binary protocol, you're forced to think about:
- ordering
- retries
- duplicates
- timeouts
- partial messages
- connection failures
- version compatibility
- capabilities
- streaming
- backpressure
- authentication
- resource limits
- concurrency
- state machines
In other words:
binary protocol design is distributed systems design wearing a smaller hat.
Two machines are communicating across a network.
They don't share memory.
They don't share clocks.
They can fail independently.
Messages can be delayed.
Connections can disappear.
Processes can restart.
Versions can differ.
And therefore, your protocol becomes the contract that keeps two independently evolving systems synchronized.
26. Backpressure
Imagine a client requests a million records.
The server begins producing:
record 1
record 2
record 3
...
record 1,000,000
What if the client can only process:
10,000 records/sec
while the server produces:
100,000 records/sec
You have a buffer problem.
A modern streaming protocol may therefore support flow control.
For example:
WINDOW_UPDATE
The client says:
I can accept 1 MB more.
The server sends until that window is consumed.
Then waits.
This prevents fast producers from overwhelming slow consumers.
And suddenly our little binary protocol starts looking suspiciously like a miniature transport protocol.
Because it is.
27. Keep the Protocol Small
There is a temptation when designing protocols to add everything.
You start with:
REQUEST
RESPONSE
Then:
STREAM
PING
PONG
AUTH
COMPRESS
ENCRYPT
WINDOW_UPDATE
PRIORITY
MULTICAST
TRANSACTION
BATCH
CACHE_HINT
TRACE
DEBUG
...
Eventually you have created:
HTTP 2: The Revenge.
Protocol complexity is dangerous.
Every feature adds:
- implementation complexity
- testing requirements
- interoperability problems
- documentation
- security surface
- future compatibility obligations
A protocol should have a small core.
Then optional capabilities can be negotiated.
Think:
Core protocol
+
extensions
rather than:
everything protocol
28. A Possible Modern Binary API
Let's put the ideas together.
Imagine a protocol called:
MAPI
Modern API Protocol.
A frame might look like:
+--------+---------+-------+------+----------+--------+---------+
| Magic | Version | Flags | Type | Request | Length | Payload |
+--------+---------+-------+------+----------+--------+---------+
| 2 B | 1 B | 1 B | 2 B | 8 B | 4 B | N B |
+--------+---------+-------+------+----------+--------+---------+
Where:
Magic:
0xDA7A
Version:
1
Flags:
STREAM
FINAL
COMPRESSED
ERROR
Type:
HELLO
AUTH
REQUEST
RESPONSE
ERROR
PING
PONG
CLOSE
Request:
uint64
Length:
uint32
Then a request payload might contain:
method
resource
parameters
encoded with TLV.
Conceptually:
REQUEST
|
+-- field 1: resource = "users"
|
+-- field 2: operation = GET
|
+-- field 3: user_id = 42
The response:
RESPONSE
|
+-- field 1: status = OK
|
+-- field 2: user_id = 42
|
+-- field 3: name = "Derek"
Notice something interesting.
We haven't actually eliminated the API.
We've simply moved the API beneath the human-readable representation.
29. Binary Doesn't Mean You Should Abandon HTTP
This is another important distinction.
You can run a binary protocol:
directly over TCP
But you don't necessarily need to.
You could have:
HTTP
↓
binary payload
or:
HTTP/2
↓
binary frames
or:
HTTP/3 / QUIC
↓
binary application protocol
The transport and application protocol solve different problems.
The transport handles things like:
delivery
ordering
congestion
connections
while the application protocol handles:
meaning
operations
schemas
requests
responses
Separating these concerns gives you flexibility.
30. Observability Becomes More Important
One downside of binary protocols is that developers cannot simply:
curl
and understand everything.
If your packet looks like:
DA 7A 01 04 00 02 ...
good luck.
So tooling becomes part of the protocol ecosystem.
You want tools capable of:
capture packet
↓
decode frame
↓
decode message
↓
display semantic fields
For example:
REQUEST
request_id: 98231
operation: GET_USER
user_id: 42
This is one reason protocol design should include developer tooling from day one.
A protocol without debugging tools becomes tribal knowledge.
And tribal knowledge does not scale.
31. Make the Protocol Testable
A serious protocol needs more than unit tests.
You want:
Golden tests
Given:
object
produce exactly:
bytes
And vice versa.
Fuzz testing
Feed random bytes into the decoder.
The decoder should:
reject safely
not:
crash
Compatibility testing
Test:
old client ↔ new server
new client ↔ old server
Property testing
Verify:
decode(encode(x)) == x
for valid values.
Boundary testing
Try:
length = 0
length = 1
length = MAX
length = MAX + 1
and:
empty string
maximum string
invalid UTF-8
unknown fields
duplicate fields
Protocol correctness lives in the edge cases.
32. Documentation Should Describe Bytes and Meaning
A protocol specification should not simply say:
Send a user request.
It should say something closer to:
Offset Size Field
0 2 Magic
2 1 Version
3 1 Flags
4 2 Message Type
6 8 Request ID
14 4 Payload Length
18 N Payload
Then:
Message Type 0x0001 = GET_USER
And:
Payload Field 0x01 = user_id
Type = UINT64
Required = yes
This is the difference between:
documentation
and:
a protocol specification.
The second allows independent implementations.
That is the real test.
Could another developer, using only the specification, implement the protocol without reading your source code?
If yes, you have designed a protocol.
33. The Most Dangerous Protocol Is the One That "Just Works"
There is a strange phase in software development where a protocol feels perfect because it works.
The client sends bytes.
The server responds.
Everything is wonderful.
Then six months later:
We need another field.
Then:
We need another client language.
Then:
We need streaming.
Then:
We need compression.
Then:
We need retries.
Then:
We need to support clients from two years ago.
Then:
Why does version 4 interpret this field differently?
The protocol wasn't designed to evolve.
It was designed to survive the demo.
These are very different goals.
34. Protocol Design Is About Future You
When designing a binary protocol, you're not only communicating with today's client.
You're communicating with:
future clients
future servers
future developers
future languages
future hardware
future network conditions
You need to leave room.
Reserve message types.
Reserve flags.
Allow unknown fields.
Define compatibility rules.
Specify maximum values.
Separate transport from semantics.
Define errors.
Define state transitions.
Document everything.
Because the protocol you design today may outlive the software that created it.
That is both terrifying and beautiful.
35. The Byte Is the Foundation
We often talk about APIs at the level of:
GET /users
POST /orders
DELETE /payments
But these abstractions hide something profound.
Every request eventually becomes bytes.
Every response eventually becomes bytes.
Every distributed system is, at some level, a collection of machines exchanging structured sequences of bytes.
The sophistication of the application does not change that.
AI models still receive bytes.
Databases still receive bytes.
Payment systems still receive bytes.
Game servers still receive bytes.
Cloud infrastructure still receives bytes.
The abstraction layer is useful because humans don't want to think about every byte.
But sometimes you need to go back down.
Not because abstraction is bad.
Because understanding the lower layer makes the higher layer better.
36. The Ideal Modern Binary Protocol
If I were designing a binary protocol for a modern API, I would want a few properties above everything else.
1. Explicit framing
The receiver should always know where a message begins and ends.
2. Strong bounds
Every length must have a maximum.
3. Explicit types
Don't encode everything as strings.
4. Stable identifiers
Operations and fields should have stable numeric IDs.
5. Forward compatibility
Unknown fields should be safely skippable where appropriate.
6. Request correlation
Every request should have a correlation mechanism.
7. Streaming support
The protocol should understand multi-message responses.
8. Capability negotiation
Compression, features, and optional behavior should be negotiated.
9. Explicit errors
Machines should receive error codes, humans can receive messages.
10. Secure transport
Do not confuse binary encoding with security.
11. Backpressure
Fast producers should not destroy slow consumers.
12. Excellent tooling
Developers need ways to inspect the bytes.
13. Fuzzability
Malformed input should be expected.
14. Documentation
The wire format should be implementable independently.
15. Small core
The protocol should not become a programming language.
37. Final Thought: APIs Are Contracts, Protocols Are Physics
An API says:
What are we allowed to ask each other?
A protocol says:
How do we physically express that conversation?
The API is the vocabulary.
The protocol is the grammar.
The network is the medium.
The bytes are the atoms.
And the distributed system emerges from the rules connecting them.
This is why binary protocol design is much more interesting than simply replacing JSON with some compact encoding.
You're designing a miniature communication system.
You're deciding how machines identify one another.
How they frame messages.
How they negotiate capabilities.
How they evolve.
How they recover.
How they stream.
How they fail.
How they handle unknown information.
How they protect themselves from malformed input.
How they remain compatible while the software around them changes.
And perhaps the most important lesson is this:
A protocol should not merely optimize today's messages. It should create a stable language for tomorrow's machines.
JSON made APIs accessible because humans could understand the messages.
Binary protocols can make APIs efficient because machines can understand the messages directly.
But the best protocol isn't necessarily the smallest.
It isn't necessarily the fastest.
It isn't necessarily the most clever.
The best protocol is the one that creates a clear, predictable, evolvable, and defensible contract between independent systems.
Because at the end of the day, modern APIs aren't really about URLs.
They aren't really about JSON.
They aren't even really about HTTP.
They are about communication.
And communication, when you strip away all the abstractions, is one machine looking at a sequence of bytes and saying:
"I know what you mean."
Top comments (0)