Email me

Explainer · 8 min read

Filed under App development Rust

UUIDv7 vs ULID: which time-sorted ID should you use?

Short answer

They are close cousins: both are 128-bit IDs that start with a 48-bit millisecond timestamp, so both sort by creation time and both keep database inserts fast. Pick UUIDv7 for database keys: it is an IETF standard (RFC 9562, May 2024), PostgreSQL 18 generates it natively, and it fits every uuid column. Pick ULID when the ID lives mostly as a string and you want 26 characters instead of 36.

Glowing bar chart titled UUIDv7 vs ULID: same speed, different size. SQLite, 2 million rows, ID as primary key, database size in megabytes: UUIDv7 stored as 36-character text 190.6, ULID stored as 26-character text 167.3, UUIDv7 stored as 16 bytes 143.9, ULID stored as 16 bytes 143.9.
Stored as 16 bytes, the two are identical in size. Stored as text, ULID's 26 characters beat UUID's 36, but both lose to bytes. Insert time was about 2 seconds per 2 million rows for all four. Rendered by Tensorust with the Proof Engine from our benchmark, Oct 24 2026 (data in the CSV).

Part of the article Pick a UUIDv7 library that keeps order: 10 tested, .NET fails 50%. This page answers one question from it in depth.

On this page · 9 sections
  1. Key facts
  2. The layouts, bit by bit
  3. What we measured
  4. Which one is winning
  5. When to pick which
  6. Common misconceptions
  7. Our take
  8. FAQ
  9. Sources

In our test on a 2-million-row SQLite table, the two inserted at the same speed. The only measurable difference was size when stored as text: 167.3 MB for ULID vs 190.6 MB for UUIDv7. Stored as 16 bytes, both came to 143.9 MB.

Key facts

What UUIDv7 ULID Source (checked Oct 24 2026)
Size 128 bits 128 bits RFC 9562, ULID spec
Timestamp 48-bit Unix ms 48-bit Unix ms same
Random bits 74 (plus 6 fixed version/variant bits) 80 same
Text form 36 chars, hex with hyphens 26 chars, Crockford base32 same
Standard IETF RFC 9562, May 2024 Community spec on GitHub, 2016 same
Native in PostgreSQL 18 uuidv7() no PostgreSQL 18 docs
Insert 2M rows, SQLite, as 16 bytes ~2.1 s ~2.2 s our test, Oct 24 2026
Database size, 2M rows, as text 190.6 MB 167.3 MB our test
Database size, 2M rows, as 16 bytes 143.9 MB 143.9 MB our test
ULIDs that look like valid UUIDv7 n/a 1.56% of 1M our test
npm weekly downloads (Oct 16-22 2026) uuid 187.3M, uuidv7 2.0M ulid 7.6M, ulidx 0.5M npm downloads API

Download the data (CSV): time and database size for all four storage formats.

The layouts, bit by bit

Diagram titled UUIDv7 vs ULID: the same 128 bits, cut differently. UUIDv7 row: unix_ts_ms 48 bits, ver 4 bits, rand_a 12 bits, var 2 bits, rand_b 62 bits; example text 01a16d80-6b4d-7cb2-8781-bebb628701a6, 36 characters. ULID row: timestamp 48 bits, randomness 80 bits; example text 01M5PR0TTD91467DVE4ZBEA3RQ, 26 characters, Crockford base32.
Both examples were generated in the same millisecond (Unix time 1792838495053), so their first 48 bits are identical. Diagram: Tensorust, from RFC 9562 section 5.7 and the ULID spec.

The first 48 bits are the same idea in both: milliseconds since January 1, 1970, which runs out around the year 10889. After that they split:

  • UUIDv7 spends 6 bits on labels: a 4-bit version (7) and a 2-bit variant. Those labels are what let any UUID parser know what it is holding. The other 74 bits are random or, if the generator wants strict ordering inside one millisecond, partly a counter (RFC 9562, section 6.2).
  • ULID spends nothing on labels: all 80 remaining bits are random. Its spec defines a "monotonic" mode that increments the random part for IDs made in the same millisecond, and says generation should fail if that part overflows.

ULID came first. Alizain Feerasta published it in the ulid/javascript repo in June 2016, as a fix for random UUIDv4 keys scattering across indexes. RFC 9562, by Kyzer Davis, Brad Peabody and Paul Leach, standardized the same idea as UUIDv7 eight years later.

What we measured

We wrote both generators straight from the specs in Python (no third-party library, so no library quirks) and ran four tables of 2 million rows each in SQLite, the ID as the primary key of a WITHOUT ROWID table, a 40-byte payload per row, on one Windows PC on October 24, 2026.

  1. Insert speed: a tie. About 2.0 to 2.2 seconds per 2 million rows for all four formats, once ID generation time is subtracted. Both IDs are time-ordered, so every insert lands at the end of the B-tree. The big speed gap is between ordered and random IDs: v4 was 16 times slower in our UUIDv7 vs UUIDv4 test.
  2. Size as bytes: identical. 143.9 MB each. Sixteen bytes is sixteen bytes.
  3. Size as text: ULID wins, bytes win more. 26-character ULIDs made a 167.3 MB file (16% bigger than bytes); 36-character UUID strings made 190.6 MB (32% bigger). If your database has a real uuid or BINARY(16) type, the text-length argument for ULID disappears.
  4. Sorting as strings: both work. We generated 28,572 IDs spread over 200 seconds; sorting the canonical strings gave exactly the time order for both formats.
  5. A ULID is not a UUIDv7. Read 1 million ULIDs as UUIDs: 25.0% had the RFC variant bits by chance, and 1.56% (1 in 64) also had the version-7 nibble. The other 98.4% are "UUIDs" of no defined version, so PostgreSQL's uuid_extract_timestamp(), which returns null for anything but versions 1 and 7, cannot read their time, and the 1 in 64 that land on version 1 would give a wrong one.

Which one is winning

Glowing bar chart titled Weekly npm downloads, Oct 16 to 22 2026, in millions: uuid 187.3, ulid 7.59, uuidv7 1.99, ulidx 0.51.
The general uuid package (which has generated v7 since version 10) dwarfs everything. The ulid package still pulls 7.6 million downloads a week. Data: api.npmjs.org, week of Oct 16 to 22 2026.

ULID has a big installed base: 7.6 million npm downloads a week for ulid, 3.0 million a week on PyPI for python-ulid, and 43.3 million all-time downloads of the Rust ulid crate. But the general UUID libraries now generate v7 too, and they are far larger: the Rust uuid crate has 913 million downloads, npm's uuid 187 million a week. New databases are adding UUIDv7, not ULID: PostgreSQL 18 ships uuidv7() and has no ULID function. Our test of 10 UUIDv7 libraries covers which v7 implementations actually keep order.

Screenshot of the github.com/ulid/spec repository: The canonical spec for ulid, 10.8k stars, 182 forks, 41 commits, the last README merge 7 years ago, 38 open issues and 36 open pull requests.
The ULID spec is a GitHub README with 10.8k stars; its last content change was years ago, with 36 pull requests open. That is stability or neglect, depending on your view. Screenshot of github.com/ulid/spec, captured Oct 24 2026.

When to pick which

  • Database primary keys, new project: UUIDv7. Native type, native generator in PostgreSQL 18, every ORM understands it.
  • IDs people see, copy or put in URLs: ULID's 26 characters are shorter, case-insensitive and avoid I, L, O and U, so they survive being read aloud. You can also get this from UUIDv7 by displaying it in base32, which some libraries do.
  • Existing ULID system: keep it. There is no performance reason to migrate, as our numbers show.
  • Mixing them in one column: avoid. The column will accept both, but tools that read timestamps or versions will treat the ULIDs as garbage.

Common misconceptions

Myth

"ULID is faster than UUID."

The recordTrue only against random UUIDv4. Against UUIDv7, our 2-million-row test showed no speed difference; both append to the end of the index.

Myth

"ULID is just a UUID in a different encoding."

The recordIt is the same size, but not the same bits. A real UUIDv7 has fixed version and variant bits; a ULID does not, so only 1 in 64 ULIDs parse as a valid v7.

Myth

"UUIDv7 leaks less than ULID."

The recordBoth reveal when the ID was created to the millisecond. If that matters, use UUIDv4. And neither stops someone reading another user's data if your app has no access checks; guessable IDs are rarely the real bug.

FAQ

What is the difference between UUIDv7 and ULID?

Both are 128-bit IDs that start with a 48-bit Unix timestamp in milliseconds, so both sort by creation time. UUIDv7 is an IETF standard (RFC 9562, May 2024) with 74 random bits and fixed version and variant bits, written as 36 hex characters. ULID is a community spec from 2016 with 80 random bits, written as 26 Crockford base32 characters.

Is ULID faster than UUIDv7?

Not in our test. Inserting 2 million rows into a SQLite table keyed by the ID took about 2 seconds for both, whether stored as 16 bytes or as text. Both are time-ordered, so both append to the end of the index. The difference is size: as text, ULID used 167.3 MB against 190.6 MB for UUIDv7; as 16 bytes, both used 143.9 MB.

Can I store a ULID in a UUID column?

Yes, both are 128 bits, and databases such as PostgreSQL accept any 128 bits in a uuid column. But the result is not a valid UUIDv7: in our sample of 1 million ULIDs, only 1.56% happened to carry the version 7 and variant bits, so functions like PostgreSQL's uuid_extract_timestamp return null for almost all of them.

Should I use ULID or UUIDv7 in 2026?

For new database keys, UUIDv7: it is a standard, PostgreSQL 18 generates it natively, and every uuid column and library understands it. ULID still fits when the ID is mostly a string, for example in URLs or file names, and you want 26 case-insensitive characters instead of 36.

Do ULIDs sort correctly as strings?

Yes, in their canonical uppercase form: in our check of 28,572 IDs over 200 seconds of timestamps, string order matched time order for both ULID and UUIDv7. Mixing upper and lower case breaks it, because ULID decoding ignores case but string sorting does not.

Sources

All checked October 24, 2026.

Specifications

Download counts (pulled Oct 24 2026)

Our benchmark

  • Tensorust, Oct 24 2026: spec-faithful UUIDv7 and ULID generators in Python 3 (os.urandom for randomness), SQLite via Python's sqlite3, CREATE TABLE t(id <BLOB|TEXT> PRIMARY KEY, payload TEXT) WITHOUT ROWID, 2,000,000 rows in 50,000-row batches, journal and sync off, one Windows 11 PC. Insert time estimated as total time minus a separate generation-only pass. Format checks on 1,000,000 ULIDs and 28,572 timestamp-spread pairs. Results in data.csv.

Related on Tensorust

Next question

Every number on this page links to its source and was checked on Oct 24, 2026. Spot an error or a newer figure? Email matt@tensorust.com and it gets fixed. Related: UUIDv7 generator and decoder, Snowflake ID decoder.

Read next

Back to The Manual →   All articles →

Get the next one by email.

What my AI agents tried this week, with the real numbers: what worked, what flopped, what made money. One email when there is something worth reading.

No spam, never sold. Every email has a one-click unsubscribe.

Up next

The article this answer comes fromPick a UUIDv7 library that keeps order: 10 tested, .NET fails 50%We tested 10 UUIDv7 libraries with 1M IDs each and a clock jump. .NET put 50% out of order, uuid6 ran 16.6 min ahead. The 2 that passed, plus free data.Read it next →

Get the receipts by email.

What my AI agents tried this week, with the real numbers: what worked, what flopped, what made money. One email when there is something worth reading.

No spam, never sold. Every email has a one-click unsubscribe.

Cite this

Free to cite and reuse under CC BY 4.0 with a link back. How it was collected: methods. Spotted an error? Corrections.

Did this answer your question?

Popular in the shop

All 124 tools →

Browse: Bundles and kitsHome, family and schoolMoney and businessAI toolsSports and fitnessMaking and hobbiesFood and drinkIndie dev and launchTrading and quant

Free: Landing page teardown iPhone app cost calculator LLM API cost calculator Paid online: $49 Launch Check App builds Articles FAQ

Disagree, or have a number to add? Discuss this on The Floor →