This page is the normative statement of what a UXID string is: how a producer lays out the bits, which characters it emits, and what a decoder reads back. An implementation in another language that follows this page produces and reads the same strings as this library. It describes the format as uxid 2.10.0 ships it.
The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be read as described in RFC 2119.
Shape of an id
A UXID string is an optional prefix and a body:
id = [ prefix delimiter ] body
body = time-body / deterministic-body- A time body is a time field followed by a random field. Its first
character is never
zorZ. - A deterministic body is the marker character
z(orZ) followed by hash characters. It carries no time.
Every body character is a character of the alphabet below. The body carries no separators, no padding and no check symbol.
Alphabet and case
The body uses Crockford Base32: 32 symbols, each standing for a 5-bit value.
| Value | Symbol | Value | Symbol | Value | Symbol | Value | Symbol |
|---|---|---|---|---|---|---|---|
| 0 | 0 | 8 | 8 | 16 | G | 24 | R |
| 1 | 1 | 9 | 9 | 17 | H | 25 | S |
| 2 | 2 | 10 | A | 18 | J | 26 | T |
| 3 | 3 | 11 | B | 19 | K | 27 | V |
| 4 | 4 | 12 | C | 20 | M | 28 | W |
| 5 | 5 | 13 | D | 21 | N | 29 | X |
| 6 | 6 | 14 | E | 22 | P | 30 | Y |
| 7 | 7 | 15 | F | 23 | Q | 31 | Z |
The letters I, L, O and U are not symbols.
- Case is display only. A producer MUST emit every body character in one case, lower or upper; this library emits lower case unless configured otherwise. An upper-case body and its lower-case spelling are the same id.
- A decoder MUST accept either case, and MUST read a symbol of either case as the same value.
- A decoder MUST NOT apply Crockford's reading substitutions (
IorLas1,Oas0) and MUST NOT skip hyphens: any character outside the table, in either case, is refused (see Refusals). - Case never applies to the prefix: a producer emits the prefix as given.
Because the symbols are in ASCII order within one case, comparing two bodies of the same case character by character compares the values they encode.
Bit packing
A field is written most significant bit first, cut into 5-bit groups, one character per group.
The partial-group rule. When a field's width is not a multiple of 5, one
group is shorter than 5 bits. That group's character is the group's raw value,
not the value shifted left or padded with zero bits: a group of k bits holds a
value from 0 to 2^k - 1 and is written as that symbol.
- In the standard time field the short group is the first one (3 bits).
- In the random field the short group is the last one.
For example, the 16-bit random field 0xFFFF is the groups 11111 11111 11111 1
and encodes as zzz1. A padding encoding (RFC 4648 style) would write the last
group as 10000 (g); a UXID producer MUST NOT.
Time field
The time is a count of milliseconds since the Unix epoch, an unsigned integer.
Standard time is 48 bits, written as 10 characters: one group of 3 bits,
then nine groups of 5 bits. A producer MUST supply a time from 0 to 2^48 - 1.
The first character is therefore always one of 0 to 7.
Compact time is the low 40 bits of the millisecond time (the time modulo 2^40), written as 8 groups of 5 bits with no short group.
- A producer MUST refuse a compact time whose first character would have the
value 31 (the low 40 bits at or above 31 x 2^35): that symbol is the
deterministic marker. In wall-clock terms the refusal applies from mid-2038
until the 40-bit window rolls over in 2039. This library raises
ArgumentError; a standard time is never refused for this reason. - The high bits of the time are not on the wire; see Decoding the time.
For the time 1700000000000 ms:
| Form | Bits | Characters |
|---|---|---|
| standard | 000 00001 10001 01111 00111 11110 01010 11010 00000 00000 | 01hf7yat00 |
| compact | 10001 01111 00111 11110 01010 11010 00000 00000 | hf7yat00 |
Random field
The random field is a whole number of bytes, written as ceil(8 x bytes / 5)
characters under the partial-group rule. A producer MUST fill it from a
cryptographically secure random source, except under monotonic generation
(below), which derives later values in a millisecond from an earlier seed.
A random field MUST NOT be wider than 10 bytes at any size.
Sizes
A size fixes the time form and the random width. Compact time hands the 8 bits it frees from the time to the random field, so a compact id carries the same number of bits as a standard one of its size, except at xl.
| Size | Mode | Time bits | Random bytes | Random chars | Total bytes | Total bits | Body length |
|---|---|---|---|---|---|---|---|
| xs | standard | 48 | 0 | 0 | 6 | 48 | 10 |
| s | standard | 48 | 2 | 4 | 8 | 64 | 14 |
| m | standard | 48 | 5 | 8 | 11 | 88 | 18 |
| l | standard | 48 | 7 | 12 | 13 | 104 | 22 |
| xl | standard | 48 | 10 | 16 | 16 | 128 | 26 |
| xs | compact | 40 | 1 | 2 | 6 | 48 | 10 |
| s | compact | 40 | 3 | 5 | 8 | 64 | 13 |
| m | compact | 40 | 6 | 10 | 11 | 88 | 18 |
| l | compact | 40 | 8 | 13 | 13 | 104 | 21 |
| xl | compact | 40 | 10 | 16 | 15 | 120 | 24 |
Compact xl is the one compact row that does not gain a random byte: the random field is capped at 10 bytes, so compact xl is 8 bits shorter than standard xl.
The body lengths are the time characters plus the random characters. The default size is xl, standard. A body generated with an explicit random width outside this table has the same layout, but its length is not a table entry and a decoder refuses it.
Prefix and delimiter
- The delimiter is a non-empty string,
_unless configured otherwise. Producer and decoder MUST agree on it; it is not recorded in the id. - The prefix is any string. An empty prefix and an absent prefix are the same thing: no prefix. For no prefix a producer MUST emit the bare body, with no delimiter.
- A prefix MAY contain the delimiter (
a_bis a valid prefix under_). - A decoder MUST take the body as everything after the last occurrence of the delimiter, and the prefix as everything before it. A string with no delimiter is a body with no prefix.
- A string that starts with the delimiter is not one a producer emits; this library's decoder reports its prefix as the empty string, and a reader SHOULD treat that as no prefix.
- A delimiter made of alphabet characters makes the split above ambiguous and is not supported.
For example, under _: cus_01hf7yat000001 is prefix cus, body
01hf7yat000001; a_b_01hf7yat000001 is prefix a_b, the same body.
Deterministic bodies
A deterministic body is a hash of a caller-supplied string, so the same inputs always give the same id. Its layout:
- The hash input is the byte string
prefix || 0x00 || from || 0x00 || count, whereprefixis the prefix's bytes (empty for no prefix),fromis the input string's bytes,0x00is one zero byte, andcountis one byte holding the number of hash characters for the size. - The digest is SHA-256 of that input.
- The body is the marker followed by the first
count x 5bits of the digest, most significant bit first, ascountcharacters of 5 bits (no short group).
| Size | Hash characters (count) | Hash bits | Body length |
|---|---|---|---|
| xs | 9 | 45 | 10 |
| s | 13 | 65 | 14 |
| m | 17 | 85 | 18 |
| l | 21 | 105 | 22 |
| xl | 25 | 125 | 26 |
- The marker is
zin a lower-case id andZin an upper-case one. Case is not part of the hash input. - Deterministic bodies use the standard body lengths; there is no compact deterministic form.
- Because
countis in the hash input, the same string at two sizes gives unrelated bodies, not one a truncation of the other. Because the prefix is, the same string under two prefixes gives unrelated bodies. - A producer MUST refuse a request that asks for both a deterministic body and monotonic generation.
Why the marker sorts last. The marker is value 31, the highest symbol. A standard time body starts with a value from 0 to 7, and a compact one with a value from 0 to 30 (31 is refused above), so within one prefix and one case every deterministic id sorts after every time-based id.
These three ids lock the format; an implementation MUST reproduce them:
| Prefix | from | Size | Id |
|---|---|---|---|
usr | alice@example.com | xl | usr_zcvt7epac0t1ebcsjfyf7cwz25 |
cus | acme-corp | s | cus_zt9vnyfd85bff0 |
| none | alice@example.com | m | zqz4cwzgyw8ks97869 |
For the first, the hash input is the bytes of usr, 0x00, the bytes of
alice@example.com, 0x00, and 0x19 (25).
Monotonic generation
Monotonic generation keeps ids unique and in order within one millisecond from one generator. It changes how the random field's value is chosen, never the layout: a monotonic id is a time body of the same size and length, and nothing in it says it was generated monotonically. A decoder reads it as any other time body.
A producer that offers it MUST behave as follows. State is kept per generator
(in this library, per process), per prefix and per random width, as the last
time T and the last random value R, the random field read as one big-endian
unsigned integer of w bytes.
- With no state, or a current time later than
T, the random field is a fresh secure random value, and the id carries the current time. - With a current time equal to or earlier than
T, the id carriesT(not the current time) and the random valueR + step, wherestepis drawn uniformly from 1 to 2^(4w) inclusive (half the field's bit width). - When
R + stepis 2^(8w) or more, the id carriesT + 1and a fresh secure random value instead.
The step is not a plus-one counter: consecutive ids in a millisecond are spread across a window of 2^(4w) values. That removes trivial enumeration, but it is a mitigation, not unpredictability; see Why monotonic mode exists. A monotonic xs uses compact time so that it has a 1-byte field to step; this library refuses a monotonic xs with an explicit standard time.
Decoding
A decoder reads a string to: the prefix (or none), the size, whether the time is compact, whether the body is deterministic, and the time (none for a deterministic body). It does not report the random field: this library answers the random field and its width as not supported on decode.
Length to size
The body length alone gives the size and the time form. This is the length table. The length is counted in bytes: every body character a producer emits is one ASCII byte, and counting bytes gives every decoder the same length for any other input too.
| Body length (bytes) | Size | Mode |
|---|---|---|
| 10 | xs | standard |
| 13 | s | compact |
| 14 | s | standard |
| 18 | m | standard |
| 21 | l | compact |
| 22 | l | standard |
| 24 | xl | compact |
| 26 | xl | standard |
The 10 and 18 ambiguities. A compact xs body and a compact m body have the lengths of a standard xs and a standard m body (10 and 18). A decoder MUST read both lengths as standard. The size it reports is then right, but a compact xs or compact m id decodes with a wrong time: its 8 time characters and the first 2 random characters are read as a 10-character standard time. A producer that needs the time back SHOULD NOT use compact xs or compact m. A monotonic xs is compact, so its time does not decode either.
The marker on decode
A body whose first character is z or Z is deterministic. The decoder reports
its size from the length (always a standard length) and no time.
Decoding the time
- Standard time: the first 10 characters are the 48-bit time, read with the 3-bit first group. The decoder takes the first character's low 3 bits; a producer never emits a first character above 7.
- Compact time: the first 8 characters are the low 40 bits of the time. The decoder supplies the high bits from its own clock: it combines the clock's bits above bit 40 with the 40 decoded bits, and when that result is more than 86400000 ms (one day) ahead of its clock, it subtracts 2^40. The time is exact while the id and the decoder's clock fall in the same 2^40 ms window (about 34.8 years) and the id is no more than one day ahead of the clock. An id from the previous window is recovered by the subtraction when it is less than 2^40 ms minus one day older than the clock.
Refusals
A body is well formed when every character is in the alphabet (either case) and its length in bytes is in the length table above. A decoder MUST decode a well-formed body and MUST refuse any other input with one of these reasons. The rule is the same for deterministic bodies: a body starting with the marker is decoded only when it is well formed.
| Reason | When |
|---|---|
empty | the body is empty (an empty string, or one ending in the delimiter) |
invalid_character | a body character is not in the alphabet in either case |
invalid_length | the body length in bytes is not in the length table |
A body that fails more than one rule is refused for the first it fails, checked
in this order: empty, invalid_length, invalid_character.
The reasons are a closed set of names, not messages. In this library
UXID.decode/1 answers {:ok, codec} or {:error, reason} with the reason as
an atom.
Generation errors
A producer refuses a request whose options contradict each other or the format. Each refusal has a name, from the option that caused it:
| Name | When |
|---|---|
compact_time_out_of_range | a compact time whose first character would be the marker (see Time field) |
rand_width_mismatch | a fixed random field whose byte count is not the request's random width |
rand_size_too_large | an explicit random width above 10 bytes |
from_with_monotonic | a deterministic body asked for with monotonic generation |
rand_with_from | a fixed random field asked for with a deterministic body |
rand_with_monotonic | a fixed random field asked for with monotonic generation |
The names are a closed set, not messages, and they are the names the
conformance corpus gives these refusals. This library raises ArgumentError
for each of them (a fixed random field is its rand: option); it also raises
for options this table does not name, such as a monotonic xs with an explicit
standard time.
Out of scope
These are part of this library but not of the wire format, and a conforming implementation need not provide them:
- The registry (
UXID.Registry): the compile-time map from prefix to resource and its JSON manifest. See Registry reference. - The Ecto types: storage and casting in an Ecto schema. See How to use UXIDs in Ecto schemas.
- A UUIDv7 body mode: a body carrying a UUIDv7 is not part of this format.
The sizes' collision math is in Sizes & Encoding; the deterministic
from: option is in Deterministic IDs reference.