A UXID body is a Crockford Base32 string made of two parts: a timestamp (which makes IDs K-sortable) followed by random bytes. The :size option controls how many random bytes are used, so you can spend fewer characters on low-cardinality resources and more where collision resistance matters.

T-shirt sizes

Pass :size as a t-shirt size - either the short or long alias:

SizeAliasesRandom bitsBody length
:xs:xsmall010 chars
:s:small1614 chars
:m:medium4018 chars
:l:large5622 chars
:xl:xlarge8026 chars

Body length excludes the prefix and delimiter - cus_ adds four more characters. With no :size, generation defaults to 80 random bits (the same as :xl).

UXID.generate!(prefix: "cus", size: :small)   # "cus_01eqrh884aqyy1"  (14-char body)
UXID.generate!(prefix: "cus", size: :xl)      # "cus_01kxr2jnqndq7qx9wrvhmrzrhw"

Deterministic IDs reuse these same body lengths, but spend the whole body (minus the one-character z/Z marker) on hash bits instead of a timestamp plus randomness - so a deterministic :m body is 85 hash bits, not 40.

:xs under monotonic generation

A standard :xs has 0 random bits - nothing to increment - so when monotonic generation is active compact_time is enabled automatically for :xs/:xsmall, yielding a 1-byte (8-bit) counter field. Passing an explicit compact_time: false on :xs/:xsmall with monotonic on raises an ArgumentError (there would be no field to count). This inherits the compact :xsmall time-decode ambiguity described under Compact time - uniqueness and sorting are unaffected, but decoding the timestamp back out of a monotonic :xs is unreliable.

Collision resistance

Two UXIDs can only collide if they share the same millisecond timestamp and the same random field. IDs generated in different milliseconds never collide, so collision risk is a birthday problem over the 2^(random bits) values available within a single millisecond. The more IDs you mint in one millisecond (a bulk insert, a seed script, a burst of events), the higher the chance two draw the same random field.

As a rule of thumb, the 50% collision point is around the square root of the space (2^(bits/2)). The table below shows how many same-millisecond IDs you can mint before the odds cross ~1%:

SizeRandom bitsDistinct random valuesSame-ms IDs for ~1% collision
:xs01the 2nd same-ms ID collides
:s1665,536~36
:m40~1.1 × 10¹²~150,000
:l56~7.2 × 10¹⁶~38 million
:xl80~1.2 × 10²⁴~150 billion

The numbers are per millisecond and per independent generator - spread the same burst across processes or nodes and the same-millisecond draws still share the space, so the birthday math spans all of them together.

Choosing a size

Pick a size from the peak same-millisecond burst rate, not the total row count. A table with billions of rows minted a few per millisecond is fine at :small; a bulk import that inserts 100k rows in one transaction is not. :xs carries no randomness (timestamp only), so it is only safe for genuinely singleton or externally-keyed resources.

To make a small size burst-safe within one process, see Why the small sizes need it most.

Compact time

Standard UXIDs use a 48-bit timestamp (10 characters). Compact-time mode drops that to a 40-bit timestamp (8 characters) and hands the freed 8 bits to the random field - more collision resistance, often in fewer characters:

UXID.generate!(size: :small)                     # 14-char body, 16 random bits
UXID.generate!(size: :small, compact_time: true) # 13-char body, 24 random bits
  • Reduces the timestamp from 48 bits (10 chars) to 40 bits (8 chars).
  • Frees 8 bits for randomness - e.g. :small gains 50% more (24 vs 16 bits).
  • Keeps perfect 5-bit Crockford Base32 alignment.
  • Stays K-sortable until ~mid-2038, after which the compact encoder raises: value 31 (the first char z/Z) is reserved as the deterministic-ID scheme marker, so no compact timestamp may emit it. Standard 48-bit timestamps are unaffected.
  • The decoder auto-detects the compact format from the length and reconstructs the full timestamp.

Compact time is useful for test suites that mint IDs rapidly and for small resources that need better collision resistance without growing longer. Enable it per call (compact_time: true), per Ecto field, or globally for small sizes - see the Configuration guide.

To raise every small size to a floor in a test suite, see Enforce a minimum size in tests.