UUID to a high/low 64-bit pair

high/low 64-bit pair9 sectionsUpdated

A UUID is two 64-bit halves. Java prints them signed, which is why getMostSignificantBits often returns a negative number. The bits are unchanged; only the sign convention differs.

A 128-bit identifier does not fit in a 64-bit integer, so every language that stores one as integers stores two: the high half and the low half. This page converts between the two representations and spells out the two things that break most often: the sign and the byte order.

high/low 64-bit paircomputed by the converter itself
inf81d4fae-7dec-11d0-a765-00a0c91e6bf6out{"high":17878533706586264016,"low":12062047867550723062}

The v1 from the original RFC examples

in0c5b2444-70a0-4932-980c-b4dc0d3f02b5out{"high":890345227701733682,"low":10956330850693612213}

A v4

in00000000-0000-0000-0000-000000000000out{"high":0,"low":0}

Nil UUID: both halves zero

inffffffff-ffff-ffff-ffff-ffffffffffffout{"high":18446744073709551615,"low":18446744073709551615}

Max UUID: both halves at 2^64 - 1

Open the converter: high/low 64-bit pair (unsigned)Nothing you paste leaves the browser.
On this page (9)

Why getMostSignificantBits returns a negative number#

Java has no unsigned 64-bit type. UUID#getMostSignificantBits hands back the same bits as the unsigned reading above, printed as a signed long, so any identifier whose first hex digit is 8 or higher prints negative. Nothing is lost: subtract 2^64 from the unsigned value when it is 2^63 or more, and you have Java's number.

UUIDUnsigned highJava getMostSignificantBits
f81d4fae-7dec-11d0-a765-00a0c91e6bf617878533706586264016-568210367123287600
0c5b2444-70a0-4932-980c-b4dc0d3f02b5890345227701733682890345227701733682

The second row is below 2^63, so both readings agree. That is the whole of the mystery.

The two readings this tool offers#

The unsigned reading takes each half big-endian, exactly as the identifier is written: the high word of f81d4fae-7dec-11d0-… is 0xf81d4fae7dec11d0. The signed reading takes each half little-endian and prints it as a signed integer. That is the shape a little-endian machine gives when it reinterprets the bytes in place, as ByteBuffer in LITTLE_ENDIAN order or BitConverter.ToInt64 do.

ReadingByte order inside each halfWhere you meet it
unsignedbig-endianJava and Kotlin bit patterns, Kafka, protobuf fixed64 pairs, Uuid::as_u64_pair in Rust
signedlittle-endianbyte buffers reinterpreted in place on a little-endian machine

The same pair in each language#

LanguageSplitRebuild
Javauuid.getMostSignificantBits() / getLeastSignificantBits()new UUID(msb, lsb)
Kotlinuuid.toLongs { msb, lsb -> … }Uuid.fromLongs(msb, lsb)
Rustuuid.as_u64_pair()Uuid::from_u64_pair(hi, lo)
KafkaUuid#getMostSignificantBitsnew Uuid(msb, lsb)
Gobinary.BigEndian.Uint64(u[:8])binary.BigEndian.PutUint64(u[:8], hi)
C#BitConverter.ToUInt64(bytes, 0)new Guid(bytes)
java
UUID id = UUID.fromString("f81d4fae-7dec-11d0-a765-00a0c91e6bf6");

long msb = id.getMostSignificantBits();   // -568210367123287600
long lsb = id.getLeastSignificantBits();  // -6384696206158828554

// the unsigned reading of the same bits
String hi = Long.toUnsignedString(msb);   // 17878533706586264016

UUID back = new UUID(msb, lsb);           // f81d4fae-7dec-11d0-a765-00a0c91e6bf6

The pair as a protobuf message#

A 128-bit identifier has no home in protobuf: there is no uint128, and bytes costs a length prefix and loses fixed width. Two 64-bit fields fit exactly, which is what bavix/apis declares for the whole API surface.

protobuf
syntax = "proto3";

package bavix.api.v1;

option go_package = "github.com/bavix/apis/pkg/bavix/api/v1";

// UUID is a message that represents a UUID.
//
// A UUID is composed of two 64-bit values, each representing a part of the UUID.
// The high part is stored in the high field, and the low part is stored in the
// low field.
message UUID {
    // The high part of the UUID.
    int64 high = 1;
    // The low part of the UUID.
    int64 low = 2;
}

Note the type: int64, not uint64. Protobuf has both, and the choice here follows what Go hands you, which is the reading below.

The reading that message carries#

The reference implementation reinterprets the sixteen bytes in place, eight at a time:

go
func UUID2DoubleInt(v uuid.UUID) (int64, int64) {
	return *(*int64)(unsafe.Pointer(&v[0])), *(*int64)(unsafe.Pointer(&v[8]))
}

func DoubleInt2UUID(highValue int64, lowValue int64) uuid.UUID {
	var uuidValue uuid.UUID

	*(*int64)(unsafe.Pointer(&uuidValue[0])) = highValue
	*(*int64)(unsafe.Pointer(&uuidValue[8])) = lowValue

	return uuidValue
}

On a little-endian machine, which is every amd64 and arm64 server, that reinterpretation reads each half **least significant byte first**. It is the signed reading in the converter, so the numbers on this page and the numbers that message carries are the same numbers.

UUIDhigh (signed)low (signed)
f81d4fae-7dec-11d0-a765-00a0c91e6bf6-3453719414676972040-690424266549598809
01890a5d-ac96-774b-bcce-b302099a805754379807421126760976305208841809415868
0c5b2444-70a0-4932-980c-b4dc0d3f02b53623603779236289292-5403687274121261928

Why store a UUID as two longs at all#

Two bigint columns beat a 36-character string on both storage and comparison, and a protobuf message with two fixed64 fields is 18 bytes on the wire against 36 for the text. The cost is that the identifier stops being readable, and that every service touching it has to agree on the byte order and on the sign convention. Write both down where the schema lives.

RFC 9562 § 4.1 · Variant FieldThe variant bits live in the low half, which is why the low word rarely looks random

RFC 9562 § 6.13 · DBMS and Database ConsiderationsWhat the standard says about storing UUIDs in a database

Questions people actually ask#

Why does getMostSignificantBits return a negative number?

Java prints a 64-bit value as a signed long, and the high half of most UUIDs exceeds 2^63. The bits are correct; Long.toUnsignedString gives the same value without the sign.

Can I rebuild the UUID from the two numbers?

Yes, with the same reading you wrote them in. Read the unsigned pair of f81d4fae-7dec-11d0-a765-00a0c91e6bf6 as signed and you get d011ec7d-ae4f-1df8-f66b-1ec9a00065a7: a different identifier, and nothing warns you. In code, new UUID(msb, lsb) in Java and Uuid.fromLongs in Kotlin round-trip their own halves.

Is the high half the first part of the printed UUID?

In the unsigned reading, yes: the high word is the first eight bytes, which is everything up to and including the third group of hex digits.

Browse the reference