UUID vs ULID
On this page
TL;DR. ULID and UUID v7 solve the same problem (time-ordered identifiers) using different encodings. ULID is shorter (26 chars vs 36) and case-sensitive Crockford-base32; UUID v7 is the universal standard with broader ecosystem support. For new code, prefer UUID v7 unless you specifically benefit from ULID’s shorter strings.
What they are
| UUID v7 | ULID | |
|---|---|---|
| Spec | RFC 9562 (May 2024, IETF standard) | ulid spec (community spec, 2016) |
| Bits | 128 | 128 |
| Time | 48-bit Unix-ms timestamp (front) | 48-bit Unix-ms timestamp (front) |
| Random | 74 bits | 80 bits |
| Encoding | hyphenated hex (xxxxxxxx-xxxx-...) | Crockford base32 (01ARZ3NDEKTSV4RRFFQ69G5FAV) |
| String length | 36 chars (32 + 4 hyphens) | 26 chars |
| Sortable | yes (lexicographic = chronological) | yes (lexicographic = chronological) |
| Native DB type | yes (Postgres uuid, SQL Server uniqueidentifier, etc.) | no (stored as char(26) or bytea(16)) |
| Standard library support | broad (every language) | narrow (npm packages, but no stdlib) |
Side-by-side
UUID v7: 01928a47-3b30-7c5e-9d1a-f0b8c4a7e923 ← 36 chars
ULID: 01HKZG3WQM7C2YMGRRPS9C4Z8J ← 26 chars
Both encode 128 bits. Both put a millisecond timestamp at the front. The difference is purely how they’re written down.
Where ULID wins
- Shorter strings. 26 vs 36 chars. In dense JSON or URLs this saves bandwidth.
- No hyphens. Easier to double-click-select in a terminal.
- Slightly more random bits (80 vs 74) — though both are far past the practical collision threshold.
- Designed sortable from day one — no version-byte gymnastics.
Where UUID v7 wins
- It’s an IETF standard. RFC 9562 is the formal spec; ULID is a community-maintained README.
- Native database types. Postgres
uuid, SQL Serveruniqueidentifier, MySQL 8 binary(16). ULID needschar(26)or custom binary handling. - Every language has a UUID library in its standard library (or close to it). ULID needs a third-party package in most ecosystems.
- Frameworks understand it. ASP.NET routes, Django UUIDField, ActiveRecord, Rails — all handle UUIDs. ULID needs custom serializers.
- Tools understand it. Logs, regex, IDEs, CLI utilities — UUID format is universally recognizable.
- Drop-in replacement for v4. If you’re migrating from v4, switching to v7 is a default-value change. Switching to ULID is a column-type change.
Storage comparison
| Approach | Bytes per ID | Notes |
|---|---|---|
Postgres uuid (v4 or v7) | 16 | Native, indexed, validated |
Postgres text storing ULID string | ~27 | Includes length prefix |
Postgres bytea(16) storing ULID bytes | 16 + overhead | Same size as uuid, but no validation |
SQL Server uniqueidentifier (Guid) | 16 | Native |
SQL Server char(26) storing ULID | 26 | Fixed-width, slower index |
The “ULID is shorter” advantage disappears once you store as bytes. For DB primary keys, UUID v7’s native type wins.
Performance — does ULID’s extra randomness matter?
Both v7 and ULID have a millisecond timestamp prefix. Within a single millisecond, ULID has 80 random bits, v7 has 74. Birthday-paradox 50% collision in one ms:
- ULID: ~
2^40≈ 1.1 trillion IDs - UUID v7: ~
2^37≈ 137 billion IDs
Neither will ever happen in real applications. Some v7 implementations add a monotonic counter for the same-millisecond case, eliminating even theoretical concerns.
Migration patterns
If you’re starting today and the choice is fresh:
new project + Postgres / SQL Server / MySQL → UUID v7
new project + DynamoDB / S3 keys → ULID (string-native, shorter)
existing project on UUIDs → switch v4 → v7 default
existing project on ULIDs → stay (no benefit to migrate)
For DynamoDB and S3 specifically, ULID’s shorter string and lack of hyphens makes for slightly more readable keys. For everywhere else, UUID v7 is the boring correct choice.
Code
UUID v7
import { v7 } from "uuid";
v7(); // "01928a47-3b30-7c5e-9d1a-f0b8c4a7e923"
ULID
import { ulid } from "ulid";
ulid(); // "01HKZG3WQM7C2YMGRRPS9C4Z8J"
Both are one-line generators. The library and the output format are the only difference.
Decision flowchart
Are you on Postgres / SQL Server / MySQL with native UUID columns?
├── Yes → UUID v7 (use the native type)
└── No, you're on DynamoDB / Redis / KV stores
├── Do you need string-native, no-hyphens keys? → ULID
└── Otherwise → UUID v7 (still fine, just stored as 16-byte blob)
Does your team / framework already have UUIDs everywhere?
└── Yes → UUID v7 (don't fragment your codebase)
Are you greenfield with no constraints?
└── UUID v7 (the default carries less ecosystem risk)
In practice, UUID v7 wins about 90% of the time.
What about UUID v6?
v6 is “v1 with the timestamp bytes reordered to be sortable.” It’s standardized in RFC 9562 alongside v7. Don’t use v6. It exists for back-compat with systems that need to migrate from v1; v7 is the modern choice.
Try the tools
- UUID v7 generator — instant time-ordered UUIDs
- UUID validator — decode the timestamp from any v7 UUID
- UUID as primary key — the deeper DB-design discussion
- UUID vs Nanoid — for the “I just need a short ID” case