DevTools Hub

Search tools

Search for a developer tool

UUID vs ULID vs Snowflake ID

Part of the Generators Toolkit

Every backend eventually needs an identifier that doesn't come from an auto-incrementing column — one that a client can generate before it ever talks to a server, that two services can mint independently without colliding, or that doesn't leak how many rows exist by counting up from 1. "Just use a UUID" is the default answer, and it's often right. It's also the answer that quietly causes index bloat on a table doing millions of inserts a day, which is exactly the problem ULID and Snowflake IDs each solve in a different way.

At a glance

UUID v4ULIDSnowflake
Size128 bits128 bits64 bits
Sortable by creation timeNoYesYes
Text form36-char hex-with-dashes26-char Crockford Base32Usually a plain integer
Needs coordination to generateNoNoYes — a unique machine/worker ID
StandardizedRFC 4122Community spec (ulid/spec)Twitter's original design, no formal RFC

The structural reason for every row above is the same: what each scheme spends its bits on.

UUID v4128 bits
122b

no timestamp at all — generation order and value order are unrelated

UUID v7128 bits
48b
74b

same timestamp-first idea as ULID, spent 6 bits on RFC 4122 version/variant markers

ULID128 bits
48b
80b

encoded as 26 Crockford Base32 characters, not hex-with-dashes

Snowflake64 bits
41b
10b
12b

a 64-bit integer — half the size of the other three, but needs a coordinated machine ID

timestamp random machine ID sequence fixed/unused

UUID v4 (and why v7 exists)

A standard v4 UUID is 122 bits of cryptographically random data plus 6 fixed bits that mark its version and variant per RFC 4122. That randomness is the whole point — no coordination needed, collisions are astronomically unlikely, and it's been the default unique identifier in almost every language and database for two decades. It is also, structurally, the worst possible key for a B-tree index that's built to store rows in sorted order: every insert lands at a random point in the tree instead of at the rightmost edge, which means far more page splits, worse cache locality, and a noticeably larger index on a high-write table than the same table keyed on a sequential integer. See Database Indexes Explained for the rest of that mechanism.

UUID v7, finalized in RFC 9562 in 2024, is the fix that keeps the UUID shape: the leading 48 bits become a millisecond Unix timestamp, and the remaining 74 bits (after the same 6 version/variant bits) stay random. A v7 UUID sorts chronologically as plain text or bytes, drops into any column already typed as UUID, and inserts at the end of a B-tree index the same way a sequential integer does — most of ULID's benefit, zero schema changes. If you're choosing a UUID version today for anything that becomes a primary key, v7 over v4 is close to a free upgrade.

ULID: a UUID-sized ID that sorts as text

ULID reaches the same 48-bit-timestamp-plus-randomness idea from the other direction — it was never bound by RFC 4122's version/variant bits, so all 80 remaining bits go to randomness instead of 74. The bigger practical difference is the text encoding: a ULID is 26 characters of Crockford's Base32 (it excludes I, L, O, and U to avoid characters that are easy to misread), which is both shorter than a 36-character UUID string and, unlike hex-with-dashes, sorts correctly with a plain string comparison — no need to parse it first. Most implementations also offer a monotonic mode: if two ULIDs are requested in the same millisecond, the random part increments by one instead of re-rolling, which guarantees strict ordering even for IDs minted faster than the clock can distinguish them.

Snowflake: half the size, at the cost of coordination

Twitter's original Snowflake design packs everything into a 64-bit integer: 1 unused sign bit, a 41-bit millisecond timestamp measured from a custom epoch (not 1970 — picking a recent epoch buys more useful range out of 41 bits), a 10-bit machine/worker ID, and a 12-bit per-millisecond sequence number that lets one machine mint up to 4,096 IDs in the same millisecond before it has to wait for the clock to advance. Discord's IDs use the identical layout with their own epoch and a cosmetic 5+5 split of the machine bits.

Being a real 64-bit integer instead of a 128-bit value is a genuine advantage — smaller index entries, native bigint columns in every database, and a value you can pass around without worrying about a text encoding at all. The cost is the one thing UUID and ULID don't need: every machine generating Snowflake IDs has to be assigned a machine ID that no other machine is using at the same time, which means an external coordination mechanism (a config value per host, a value handed out by a coordinator service like ZooKeeper, or a Kubernetes pod ordinal) has to exist and stay correct. Get two machines the same ID and they can mint colliding Snowflake IDs — a failure mode neither UUID nor ULID has, by design.

The actual decision

Every row in the table above comes down to three independent trade-offs, and they don't all point the same direction:

  • Do you need it to sort by creation time? A plain UUID v4 doesn't. UUID v7, ULID, and Snowflake all do, and for the same reason — a leading timestamp.
  • Can you tolerate coordinating machine identity? UUID and ULID need none; Snowflake needs a correctly assigned, non-colliding machine ID on every generator. That's a real operational cost, not a one-time setup step — it has to stay correct as machines come and go.
  • Does the size on the wire and on disk matter? Snowflake's 64 bits beat UUID/ULID's 128 at scale — half the index entry size across billions of rows is not nothing. If that's not a bottleneck for your table, it's not worth the coordination cost.

One more option: KSUID

If none of the three above fit, KSUID (K-Sortable Unique ID, from Segment.io) is worth knowing about: 160 bits — a 32-bit seconds-resolution timestamp from a custom 2014 epoch plus 128 bits of randomness — encoded as a fixed 27-character Base62 string. It trades ULID's millisecond precision for a wider timestamp range and more randomness, and its Base62 encoding (versus ULID's Base32) is marginally more compact per bit at the cost of being case-sensitive.

Practical guidance

  • Starting a new project, no existing UUID columns? Use ULID. It sorts correctly as text with zero parsing, has no version/variant tax, and needs no coordination.
  • Already have UUID-typed columns, or a library that expects RFC 4122 UUIDs? Use UUID v7. Same column type, same 36-character format everything already expects, chronologically sortable.
  • Running at a scale where index size is a measured bottleneck, and you already operate the infrastructure to assign machine IDs reliably? Snowflake's 64-bit footprint is a real win — this is the case Twitter and Discord actually built it for.
  • No timestamp requirement at all — a session token, an API key, anything where sort order is irrelevant or actively undesirable? Plain UUID v4's full randomness is fine, and simpler than reaching for a scheme built to solve a problem you don't have.

Try it yourself

UUID Generator produces v4, v1, or v7 UUIDs, and ULID Generator covers ULIDs including monotonic mode — both run entirely in your browser. Snowflake ID Generator generates and decodes Snowflake IDs against the Twitter, Discord, Unix, or a custom epoch, showing exactly how the timestamp/machine/sequence bits pack into the final integer. And Distributed ID Explorer puts all four schemes from this post side by side — generate one of each for the same instant, or paste in an ID you already have and let it auto-detect and decode whichever format it turns out to be.

Related tools