📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

📚 Interview question

What is endianness, and how do you read and write multi-byte binary data correctly in .NET?

Big-endian versus little-endian byte order, which platforms and protocols use which, network byte order, and the .NET APIs (BinaryPrimitives, BinaryReader, BitConverter, Guid) that handle byte order explicitly or implicitly.

Mid-Level Theory Intermediate binary-data binaryprimitives computer-basics endianness networking

🎯 Key points

  • Endianness is the order of bytes in a multi-byte value; it does not affect single bytes, UTF-8 text or bit order within a byte
  • Little-endian stores the least significant byte first; x64 and Arm64 platforms running .NET are little-endian
  • Big-endian (network byte order) is used by TCP/IP headers and many file formats and protocols
  • BitConverter uses the machine's byte order and GetBytes allocates; BinaryReader and BinaryWriter are always little-endian
  • BinaryPrimitives reads and writes with explicit endianness on spans, without allocation, compiling to a load plus one byte-swap instruction
  • Guid.ToByteArray() uses .NET's mixed layout; ToByteArray(bigEndian: true) and new Guid(bytes, bigEndian: true) give RFC 4122 order (available in current .NET, including .NET 10)
  • Misread byte order silently corrupts values, and on length fields can cause huge allocations or denial of service, so validate lengths read from untrusted input

⏱ 30-second answer

Endianness is the order in which the bytes of a multi-byte value are stored. Little-endian puts the least significant byte first, so 0x12345678 is stored as 78 56 34 12. Big-endian puts the most significant byte first: 12 34 56 78. x64 and Arm64 machines running .NET are little-endian, but network protocols ("network byte order") and many file formats are big-endian. In .NET I use BinaryPrimitives.ReadInt32BigEndian and friends so the byte order is explicit and portable. BitConverter follows the machine, and BinaryReader/BinaryWriter are always little-endian.

🗣 Full answer (senior)

What it is

A 32-bit integer occupies four bytes. The CPU has to choose which byte goes at the lowest address.

Value 0x12345678 addr+0 addr+1 addr+2 addr+3
Little-endian 78 56 34 12
Big-endian 12 34 56 78

It only matters when you reinterpret bytes: writing a number to a file or socket, reading a binary format, hashing, or casting a byte span to an integer. Inside your program, x << 8 and arithmetic behave the same on every platform.

Who uses what

Little-endian Big-endian
x86/x64, Arm64 as used by Windows, Linux, macOS, iOS and Android TCP/IP headers ("network byte order")
UTF-16LE (Encoding.Unicode), most Windows file formats (BMP, PE) PNG, JPEG markers, Java's DataOutputStream, Kafka and many binary protocols, X.509/ASN.1 DER
BinaryReader / BinaryWriter Many financial and telecom wire formats

The .NET APIs

  • BinaryPrimitives (System.Buffers.Binary): ReadInt32BigEndian, WriteUInt16LittleEndian, ReverseEndianness, and so on. Explicit, span-based, no allocations, and the JIT turns them into a single load plus a byte-swap instruction. This is the default choice.
  • BitConverter: GetBytes and ToInt32 use the machine's order; BitConverter.IsLittleEndian tells you which. Code that uses it for a wire format is only correct by accident.
  • BinaryReader / BinaryWriter: always little-endian, whatever the machine. Fine for your own formats, wrong for big-endian protocols.
  • IPAddress.HostToNetworkOrder / NetworkToHostOrder: older helpers for socket code.
  • Guid: ToByteArray() writes the first three groups little-endian and the rest as-is, so the bytes don't match the string form. .NET 8 added ToByteArray(bigEndian: true) and new Guid(bytes, bigEndian: true) for RFC 4122 byte order.

Production example

A .NET service read message lengths from a Java service's TCP stream with BinaryReader.ReadInt32(). Java writes big-endian, so a 300-byte message (00 00 01 2C) was read as 738,263,040 bytes; the service tried to allocate 700 MB and crashed under load. Switching to BinaryPrimitives.ReadInt32BigEndian and adding a maximum-length check fixed both the bug and the denial-of-service risk.

🔍 What the interviewer is checking

Checks low-level data literacy needed for protocols, file formats and interop.

Green flags: clear definition with an example, knows which platforms and protocols use which order, picks BinaryPrimitives, knows BitConverter and BinaryReader behaviour, mentions validating lengths.

Red flags: thinks endianness affects all data, can't name an API, would use BitConverter for a network protocol.

📚 Learn in depth

1. Picture it in memory

int value = 0x0A0B0C0D;

        lower addresses  ------------------>  higher addresses
LE:     [0D] [0C] [0B] [0A]     (least significant byte first)
BE:     [0A] [0B] [0C] [0D]     (most significant byte first, reads like the hex)

The names come from Gulliver's Travels (which end of an egg to crack). Bit order inside a byte isn't affected; you never see it from C#.

2. Where byte order sneaks in

Operation Endianness-sensitive?
Arithmetic, shifts, comparisons on int No
BitConverter.GetBytes, MemoryMarshal.Cast<byte, int> Yes, machine order
BinaryWriter.Write(int) Yes, always little-endian
Writing a length prefix on a socket Yes, the protocol decides
Hashing an integer's bytes Yes, hash differs by byte order
Encoding.Unicode vs Encoding.BigEndianUnicode Yes, UTF-16LE vs UTF-16BE
UTF-8 No, it's a byte sequence

3. How BinaryPrimitives stays fast

BinaryPrimitives.ReadInt32BigEndian(span) compiles to a 4-byte load and a bswap (x64) or rev (Arm64) instruction when the machine is little-endian, and a plain load when it already matches. There's no loop and no allocation, which is why it's used throughout Kestrel, System.Text.Json and the gRPC libraries.

4. Designing a binary format

  • Write the byte order into the specification and the code (WriteInt32BigEndian), never rely on defaults.
  • Prefer fixed-width fields or a well-known varint scheme, plus a version field.
  • Validate lengths before allocating: a corrupt or malicious length prefix shouldn't trigger a huge allocation.
  • Consider whether you need a custom format at all: Protocol Buffers, MessagePack or CBOR handle byte order and versioning for you.

5. GUIDs: the classic surprise

string form:          00112233-4455-6677-8899-AABBCCDDEEFF
ToByteArray():        33 22 11 00 55 44 77 66 88 99 AA BB CC DD EE FF
ToByteArray(true):    00 11 22 33 44 55 66 77 88 99 AA BB CC DD EE FF   (.NET 8+)

Systems that store GUIDs as 16 raw bytes (PostgreSQL uuid, Java, Python) use the big-endian layout. Mixing the layouts gives "the same" GUID different bytes, and lookups fail.

6. Is big-endian hardware still relevant?

Rarely for .NET: IBM s390x (mainframes) is big-endian and has community .NET builds. The real-world reason to care is data formats and protocols, not CPUs.

7. Try it yourself

  1. Write 0x12345678 with BitConverter.GetBytes, BinaryWriter and BinaryPrimitives.WriteInt32BigEndian, and print each result in hex.
  2. Read the width and height of a PNG file: they're big-endian uint values at byte offsets 16 and 20.
  3. Convert a Guid to bytes with and without bigEndian: true and compare them with the string form.
  4. Write a length-prefixed message reader with PipeReader or a Stream that rejects lengths over 1 MB.

💻 Code

namespace Endianness;

using System.Buffers.Binary;

public static class Program
{
    public static void Main()
    {
        Console.WriteLine($"This machine is little-endian: {BitConverter.IsLittleEndian}");

        Span<byte> buffer = stackalloc byte[4];

        BinaryPrimitives.WriteInt32BigEndian(buffer, 0x12345678);
        Console.WriteLine(Convert.ToHexString(buffer));      // 12345678

        BinaryPrimitives.WriteInt32LittleEndian(buffer, 0x12345678);
        Console.WriteLine(Convert.ToHexString(buffer));      // 78563412

        // Reading the dimensions from a PNG header (big-endian per the PNG specification)
        byte[] png = File.ReadAllBytes("logo.png");
        uint width = BinaryPrimitives.ReadUInt32BigEndian(png.AsSpan(16, 4));
        uint height = BinaryPrimitives.ReadUInt32BigEndian(png.AsSpan(20, 4));
        Console.WriteLine($"{width} x {height}");

        var id = Guid.Parse("00112233-4455-6677-8899-aabbccddeeff");
        Console.WriteLine(Convert.ToHexString(id.ToByteArray()));                 // 33221100554477668899AABBCCDDEEFF
        Console.WriteLine(Convert.ToHexString(id.ToByteArray(bigEndian: true)));  // 00112233445566778899AABBCCDDEEFF
    }
}

public static class FrameReader
{
    private const int MaxFrameLength = 1024 * 1024;

    /// Reads one big-endian length-prefixed frame from a stream.
    public static async Task<byte[]> ReadFrameAsync(Stream stream, CancellationToken ct)
    {
        byte[] header = new byte[4];
        await stream.ReadExactlyAsync(header, ct);
        int length = BinaryPrimitives.ReadInt32BigEndian(header);
        if (length is < 0 or > MaxFrameLength)
        {
            throw new InvalidDataException($"Frame length {length} is out of range.");
        }
        byte[] payload = new byte[length];
        await stream.ReadExactlyAsync(payload, ct);
        return payload;
    }
}

⚠ Common mistakes

  • Mistake: Using BitConverter for a wire or file format. Why it is wrong: It uses the machine's byte order, so the format is defined by whatever CPU wrote it and is wrong for big-endian protocols. BitConverter.GetBytes also allocates a new array for every value, which adds GC pressure when serialising millions of fields. Instead: Use BinaryPrimitives with the byte order named in the specification. BitConverter is acceptable only for data that never leaves the process or machine (for example an in-memory cache key), and even then BitConverter.TryWriteBytes(span, value) avoids the allocation.
  • Mistake: Assuming BinaryReader matches any protocol. Why it is wrong: It is always little-endian, so reading a Java or network big-endian length gives a huge or negative number. Instead: Read the bytes and decode with ReadInt32BigEndian (or the matching method).
  • Mistake: Trusting a length prefix before allocating. Why it is wrong: A wrong byte order or a malicious peer turns a small length into hundreds of megabytes, causing OutOfMemoryException or a denial of service. Instead: Validate lengths against a maximum before allocating, and read with ReadExactlyAsync.
  • Mistake: Thinking endianness affects strings or bits within a byte. Why it is wrong: UTF-8 is a byte sequence, and bit order is invisible from C#; this leads to unnecessary and wrong byte-swapping. Instead: Only worry about multi-byte numeric values and UTF-16/UTF-32 text.
  • Mistake: Exchanging GUIDs as ToByteArray() bytes with non-.NET systems. Why it is wrong: .NET's mixed layout does not match the RFC byte order other platforms use, so IDs do not match. Instead: Exchange GUIDs as strings, or use ToByteArray(bigEndian: true).
  • Mistake: Hand-written byte-swapping with shifts and masks. Why it is wrong: It is easy to get wrong for signed values and slower than the intrinsic. Instead: Use BinaryPrimitives.ReverseEndianness or the explicit read and write methods.
  • Mistake: Hand-rolling a binary format when a standard one would do. Why it is wrong: You then own byte order, versioning, schema evolution and fuzz testing yourself. Instead: Prefer Protocol Buffers, MessagePack or CBOR unless a partner's fixed layout or extreme performance forces a custom format.

🏛 Senior insight: trade-offs

Seniors treat byte order as part of the contract of a binary format, not a property of the machine. They make it explicit with BinaryPrimitives and know the failure modes of getting it wrong:

Risk Example Defence
Silent corruption ID 1 read as 16,777,216 maps to the wrong record golden-byte tests captured from the other system
Denial of service length prefix misread as 700 MB, allocation crashes the pod maximum frame length, ReadExactlyAsync, reject and close
Interop mismatch GUID bytes differ between .NET and PostgreSQL/Java exchange as strings or RFC byte order

Performance: in high-throughput parsers (Kestrel, gRPC, Kafka clients) BinaryPrimitives over Span<byte> or ReadOnlySequence<byte> with PipeReader is the standard pattern: no allocations, one instruction per field. BitConverter.GetBytes plus array copies can cost more than the business logic at hundreds of thousands of messages per second.

Custom versus standard formats: a hand-rolled format is justified for a partner's fixed layout or a measured hot path; otherwise Protobuf or MessagePack give portability, versioning and tooling for free.

🔗 Follow-ups the interviewer may ask

  1. When: Candidate recommends BinaryPrimitives over BitConverter. Why: Applies endianness in a realistic code review together with text encoding.
    How to answer

    Several real bugs. Encoding.ASCII turns José into Jos?, so names are corrupted. The field limit is 32 bytes, but the code truncates at 32 chars; once you fix the encoding to UTF-8, a 32-char name can be up to 96 bytes and overflows the buffer. Substring can also split a surrogate pair. BitConverter writes the machine's little-endian order, but the partner specification says big-endian, so every ID and length is wrong. I'd rewrite it with BinaryPrimitives, UTF-8 truncated on grapheme boundaries to 32 bytes, input validation, and golden-byte tests from the partner's specification.

  2. When: Candidate struggles to explain why a length prefix became negative. Why: Checks the basics of how integers are represented before byte order.
    How to answer

    A bit is a single 0 or 1; a byte is 8 bits and is the smallest unit memory addresses. C# numeric types have fixed sizes on every platform: byte 1 byte, short 2, int 4, long 8, Int128 16, Half 2, float 4, double 8, decimal 16. Signed integers use two's complement, so int runs from −2,147,483,648 to 2,147,483,647. Integer arithmetic is unchecked by default: int.MaxValue + 1 silently wraps to int.MinValue, while float/double never throw and produce Infinity or NaN, and decimal always throws. Make overflow explicit where a wrap would be a bug (money, counters, sizes from external input): use checked, a wider type, or generic-math conversions such as int.CreateChecked(x) and int.CreateSaturating(x).

🛠 Real-world problems: how you handle them

Java-to-.NET TCP integration crashes with OutOfMemoryException
Situation
A .NET 10 worker consumed a length-prefixed TCP feed from a Java pricing service at about 2,000 messages a second.
Signals
Intermittent `OutOfMemoryException`, container restarts, logs showing frame lengths in the hundreds of millions, and gen 2 and LOH growth in dotnet-counters just before each crash.
Investigation
Captured traffic with Wireshark and compared the first four bytes of a frame (`00 00 01 2C`, 300) with what the reader computed (738,263,040). Found `BinaryReader.ReadInt32()`, which is little-endian, reading Java's big-endian `DataOutputStream` output.
Resolution
Replaced it with `BinaryPrimitives.ReadInt32BigEndian`, added a 1 MB frame limit that closes the connection on violation, and switched to `ReadExactlyAsync`.
Prevention
Documented the wire format, including byte order, in the interface specification; added golden-byte tests that decode frames captured from the Java service; added a metric and alert on rejected frames.

How to tell it (STAR)

**Situation:** Our [pricing worker] at [company] kept crashing with out-of-memory errors when consuming a TCP feed from a Java service. **Task:** I had to find the cause and make the consumer robust. **Action:** In a packet capture the length prefix was 300 bytes, but our reader computed about 738 million. `BinaryReader` reads little-endian, while Java writes big-endian. I switched to `BinaryPrimitives.ReadInt32BigEndian`, added a maximum frame length, and wrote tests using real captured frames. **Result:** The crashes stopped, the worker handled [2,000] messages a second steadily, and the frame limit closed a denial-of-service hole we hadn't noticed.

↩ Usually asked after

More in Computer Basics

Last updated 23 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me