Why networks are big-endian but your CPU is little-endian
Two halves of the same machine disagree on which end of a number comes first. The split is older than you, and it's never going away.
On this page
The picture version
Six pictures for a reader who has never opened a memory viewer. The prose below fills in the seams the pictures skip.
1 · The problem
You stored the number four. Memory shows it backwards.
2 · Two answers, both defensible
Start from the important end, or start from the unimportant end.
3 · Where the naive assumption breaks
Just copy the bytes across, and nobody reports an error.
4 · The fix
Pick one order for the wire and make everybody convert.
5 · Why each side chose what it chose
One was picked for human eyes. The other for a hardware convenience.
6 · Keep this card
The whole thing on one index card.
Why it exists
You’ve probably had this moment: you set a variable to 4, then open the
memory view in your debugger — or dump the file with xxd — and the four
bytes read 04 00 00 00. Not 00 00 00 04. The number you just wrote looks
like it’s stored backwards, and the tooling reports this as completely normal.
That’s the running example for the rest of this post: a single 32-bit field
holding the value 4 — say, a length prefix in front of a four-byte
message. Four bytes have to be laid out in some particular order in memory and
on the wire, and nothing in the math of “a 32-bit integer” tells you which
byte goes first.
That ambiguity — which end of a multi-byte number is “first”? — is endianness. And the answer turns out to be: it depends on who you ask, and the two answers we landed on disagree.
- Your x86 laptop, your phone’s ARM core (in its usual mode), and your RISC-V box all store integers little-endian.
- The IP, TCP, UDP headers on every packet you send are big-endian.
So the moment a number crosses from your CPU’s registers to a network packet, somebody has to swap bytes. We’ve been swapping since the early 1980s. Why?
Why it matters now
Endianness is one of those things that’s invisible until you cross a boundary. The boundaries it lives on:
- The network. The multi-byte numeric fields in IP/TCP/UDP headers, DNS
messages, TLS record lengths, and gRPC’s message-length prefix are all
big-endian.
htons(),htonl(),ntohs(),ntohl()exist for exactly this. - File formats. PNG’s integers are big-endian by spec; JPEG’s marker segment lengths are too. ELF and TIFF don’t pick — they carry an endianness flag in the header and make the reader adapt. Formats that grew up next to a CPU rather than a wire (Bitmap, ZIP integers) lean little.
- Hardware registers. Memory-mapped device registers don’t care what your
CPU thinks; the device picks. Drivers are full of
cpu_to_le32/cpu_to_be32macros for this reason. - Serialization libraries. Protobuf wire format is little-endian for fixed-width fields. CBOR is big-endian. MessagePack is big-endian. The choice often tells you which world the format grew up in.
A bug in this layer doesn’t crash loudly — it produces wrong numbers. A
length field of 4 read as 67_108_864 is the kind of failure that shows up
as “the packet parser hangs forever” rather than “segfault on line 47.” That’s
why the convention exists at all: pick one, write it down, swap if you have
to.
The short answer
endianness = which byte of a multi-byte number lives at the lowest memory address
Picture to keep: four numbered mailboxes in a row, and a four-digit number to post. Big-endian drops the most important digit in box 0; little-endian drops the least important digit in box 0. The number is the same; only the direction you walk the boxes differs. (Where the picture breaks: mailboxes are labelled, so you could always look. Bytes aren’t — nothing in the four bytes says which convention wrote them, which is why the reader has to be told out-of-band and why disagreement is silent rather than loud.)
- Big-endian — most-significant byte first. Reads like a number written
on paper:
0x12345678is stored as12 34 56 78, and our length field4as00 00 00 04. - Little-endian — least-significant byte first. The same number is stored
as
78 56 34 12, and the length field as04 00 00 00.
Both work. Both have defensible arguments. The split between “network byte order is big” and “x86 is little” is a historical accident that calcified into a standard.
How it works
Start with the naive assumption: a 32-bit number is a 32-bit number, so
just copy the bytes. Take our length field, the value 4. In a register,
it’s just 32 bits — no “order” exists. The order only appears when you ask:
what byte is at address N, N+1, N+2, N+3?
addr +0 +1 +2 +3
big-endian 00 00 00 04 (matches written order)
little-endian 04 00 00 00 (low byte first)
The CPU’s load and store instructions pick one convention and bake it into
the silicon. On x86-64, MOV of a 32-bit word writes those four bytes in
little-endian order, full stop. On a big-endian machine like a classic
PowerPC or a SPARC, the same instruction would write them in the opposite
order. Same number in the register, different bytes in RAM.
Why the naive assumption breaks. Send those four bytes across a network.
The wire is a stream of bytes, no concept of “register.” The receiver picks
them up in the order they arrived. A little-endian sender writes
04 00 00 00; a receiver that reads most-significant-byte-first sees
0x04000000 — 67,108,864. It now waits for a 64-megabyte message that will
never arrive. Nothing crashed. Nothing logged an error. The parser just
hangs.
The fix the early internet adopted: pick one order for the wire and make
everyone convert. That order, defined in the early TCP/IP RFCs, is
big-endian — what we now call “network byte order.” Every host, regardless
of native endianness, swaps to big-endian on the way out and back on the way
in. Our length field goes onto the wire as 00 00 00 04 no matter who sent
it. On a big-endian host, the swap is a no-op. On a little-endian host (most
of them today), it’s a real byte-reverse.
Why big for the network?
The standard account: big-endian is what humans write. When you write
12,345, the most significant digit is leftmost — it’s big-endian
positional notation. Putting bytes on the wire most-significant-first means a
hex dump of a packet looks like the number it represents. For protocol
designers reading network traces by hand in 1981, that mattered a lot.
There’s also a small algorithmic argument: when comparing numbers lexicographically as byte strings, big-endian sorts the same way as the numbers themselves. Useful for some routing tricks, less so today.
Why little for x86?
The standard account here is more mechanical and arguably more interesting. Little-endian has a property that mattered to early hardware designers: reading a smaller integer from the start of a larger one Just Works.
Imagine you stored a 32-bit value 0x0000_00FF in little-endian: bytes
FF 00 00 00. If you load just one byte from that address, you get 0xFF.
Load two, you get 0x00FF. Load all four, you get 0x000000FF. The address
of the value is the same regardless of how wide a load you do — because the
low byte sits at the bottom. Big-endian would put the FF at the end, so
a 1-byte load at the same address gives you 0x00, not 0xFF. You’d have to
adjust the address by the difference in widths.
This made arithmetic carry propagation, pointer truncation between widths, and certain kinds of mixed-width arithmetic a hair simpler in hardware. Intel adopted little-endian for the 8086 in 1978, AMD inherited it for x86-64, and ARM made little-endian the default in practice. RISC-V is a partial exception worth stating precisely: instruction fetch is fixed little-endian, but the spec allows little-endian, big-endian, and bi-endian data accesses — real implementations are overwhelmingly little-endian anyway. The momentum, not the spec, is what’s overwhelming.
No clean primary source explains why exactly Intel picked little-endian for the 8086. The often-repeated story is the mixed-width-load argument above, but no contemporary Intel design document confirms that was the deciding factor versus, say, compatibility with the earlier 8080 and its accumulator conventions. Take the “why” with a grain of salt; the “what” is solid.
Bi-endian and the awkward middle
Some architectures — older ARM, older MIPS, IA-64, PowerPC — are bi-endian: a configuration bit selects which mode the CPU runs in. In practice almost everyone configures these as little-endian today, because that’s where the software ecosystem is. The mode bit is a relic.
Then there’s mixed-endian (“middle-endian”), which used to exist on the PDP-11 for 32-bit words and occasionally shows up in formats where one field is little-endian and another is big-endian in the same struct — typically because a format grew up on little-endian machines but embedded fields inherited from a big-endian protocol. It’s universally regarded as a footgun, for the obvious reason: correctness now depends on remembering the direction field by field. (SMB/CIFS is often named as the canonical example; the base spec actually says multi-byte fields are little-endian unless otherwise noted, so treat “SMB is mixed-endian” as lore with no clean source behind it.)
The seams
- Endianness is invisible inside one machine. If your program never
serializes, never reads files written by other architectures, and never
puns a
uint32_tinto auint8_t[4], you will never notice. The bug surface is entirely at boundaries. htonlis “host to network long” — and on x86 it’s a byte-reverse instruction. Modern x86 hasBSWAPand ARM hasREV; what was once a loop compiles down to a single instruction on those targets. Effectively free.- Floats have endianness too. IEEE 754 doesn’t specify byte order; the CPU does. Almost always the same as integer endianness on the same machine, but “almost” has bitten people writing cross-platform save files.
- UTF-16 has a BOM for exactly this reason. A
BOM
at the start of an unlabelled UTF-16 stream tells you whether to read it as
little- or big-endian. It’s optional, and it’s specifically not meant to be
used when the stream is already tagged
UTF-16LEorUTF-16BE— the label wins. UTF-8 doesn’t need one at all, because its code units are single bytes. - Big-endian survives where it had a head start. Java’s
DataOutputStreamis big-endian. JVM bytecode is big-endian. The numeric header fields of the internet protocol suite are big-endian. None of these are going to change.
The deeper observation: endianness is a coordination problem the industry solved twice — once for hosts (little wins), once for the wire (big wins) — and never reconciled. It’s cheap enough to swap that nobody has to.
You started with endianness = which byte lives at the lowest address. What
did the length-field story add? — + it is only ever a question at a boundary. Inside one machine the convention is invisible and self-consistent;
the bug surface is entirely where bytes cross from one convention’s world into
another’s, which is exactly why the fix is a conversion function and not a
better number format.
Check yourself
Before you go — a colleague argues that since almost every machine today is little-endian, network byte order is a pointless legacy tax and new protocols should just use little-endian and skip the swap. What’s the strongest counter?
Answer
Two things. First, the tax is nearly free: htonl compiles to a single
BSWAP/REV instruction, so you’re arguing about a cycle. Second — and this
is the real point — the value of a convention is that it’s fixed, not that
it’s optimal. A new protocol choosing little-endian doesn’t remove the
conversion; it adds a second convention that every parser and every hex-dump
reader now has to disambiguate per-field. That’s the SMB/CIFS failure mode.
Note the counter isn’t “big-endian is better” — it’s that switching costs more
than it saves.
And: your program stores a uint32_t and then reads the first byte of it via a
uint8_t*. Same code, x86 laptop and a big-endian machine. Same answer?
Answer
No — and this is the little-endian design argument in miniature. On
little-endian, that first byte is the low byte: for our length field 4 you
read 0x04. On big-endian, it’s the high byte: you read 0x00. This is why
the pattern is a portability bug, and also why little-endian made narrowing
loads convenient for early hardware — the address of a value doesn’t change
with the width you read it at.
Famous related terms
- Network byte order —
network byte order = big-endian, by RFC— the convention every internet protocol header obeys. htonl/ntohl—htonl = "if I'm little-endian, byte-reverse; else no-op"— the portable way to write code that doesn’t care.- BOM —
BOM = magic prefix that says which endianness this UTF-16 stream is— endianness as a runtime question instead of a compile-time one. - Bi-endian CPU —
bi-endian = same silicon + a mode bit— almost always set to little today. __builtin_bswap32—bswap = single CPU instruction that reverses byte order— what your standard library actually compileshtonldown to.
Going deeper
- Danny Cohen, On Holy Wars and a Plea for Peace (IEN 137, 1980) — the primary source, and still the best answer to “why did anyone have to pick at all?” It also named the camps, after Swift’s Lilliputians fighting over which end of a boiled egg to crack.
- Linux:
include/uapi/linux/byteorder/and thecpu_to_be32/cpu_to_le32families — the working explainer, for “what does production code actually do about this,” including how the macros compile to nothing on a matching host. - RFC 1700 and the earlier IP-suite RFCs — the rabbit hole if you want to see “network byte order = big-endian” written down at the source rather than quoted from a man page.