Last updated
What Is a ULID?
A ULID (Universally Unique Lexicographically Sortable Identifier) is a 26-character string that combines a millisecond-precision timestamp with random bits. Unlike UUID v4, ULIDs sort chronologically, making them ideal as database primary keys in distributed systems.
ULID Format Breakdown
A ULID looks like this:
01ARZ3NDEKTSV4RRFFQ69G5FAV
- Characters 1–10: 48-bit timestamp (milliseconds since Unix epoch)
- Characters 11–26: 80 bits of cryptographically secure randomness
- Total: 128 bits — same size as a UUID
- Encoding: Crockford's Base32 (no I, L, O, U to avoid ambiguity)
Generating a Single ULID
Use the generator to produce one ULID at a time. Each click produces a new, unique value:
01HN2K5QXYZ3NDEKTSV4RRFFQ6
01HN2K5QXYZ3NDEKTSV4RRFFQ7
01HN2K5QXYZ3NDEKTSV4RRFFQ8
Notice the first 10 characters are identical — these three were generated in the same millisecond. The last 16 characters differ, ensuring uniqueness.
Bulk ULID Generation
Generate 10 ULIDs at once for seeding a test database:
01HN2K5QXYZ3NDEKTSV4RRFFQ6
01HN2K5QXYZ3NDEKTSV4RRFFQ7
01HN2K5QXYZ3NDEKTSV4RRFFQ8
01HN2K5QXYZ3NDEKTSV4RRFFQ9
01HN2K5QXYZ3NDEKTSV4RRFFQA
01HN2K5QXYZ3NDEKTSV4RRFFQB
01HN2K5QXYZ3NDEKTSV4RRFFQC
01HN2K5QXYZ3NDEKTSV4RRFFQD
01HN2K5QXYZ3NDEKTSV4RRFFQE
01HN2K5QXYZ3NDEKTSV4RRFFQF
These are already in chronological order — no ORDER BY timestamp needed.
Decoding a ULID
Paste an existing ULID to extract its timestamp:
Input: 01HN2K5QXYZ3NDEKTSV4RRFFQ6
Decoded:
Timestamp component: 01HN2K5QXY
Unix timestamp (ms): 1705312847123
Human-readable time: 2024-01-15 08:20:47.123 UTC
Random component: Z3NDEKTSV4RRFFQ6
Random bits: 80 bits of entropy
Using ULIDs as Database Primary Keys
Replace UUID v4 with ULID in a PostgreSQL table:
-- UUID v4 (random, no natural sort order)
CREATE TABLE orders_old (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
customer_id INT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- ULID (sortable, no separate timestamp needed for ordering)
CREATE TABLE orders (
id CHAR(26) PRIMARY KEY, -- store as CHAR(26)
customer_id INT
);
-- Insert with ULID
INSERT INTO orders (id, customer_id)
VALUES ('01HN2K5QXYZ3NDEKTSV4RRFFQ6', 42);
-- Retrieve in creation order using just the ID
SELECT * FROM orders ORDER BY id;
ULID in JavaScript
import { ulid } from 'ulid';
// Generate a new ULID
const id = ulid();
console.log(id); // 01HN2K5QXYZ3NDEKTSV4RRFFQ6
// Generate with a specific timestamp (for testing)
const fixedId = ulid(1705312847123);
// Decode timestamp from existing ULID
import { decodeTime } from 'ulid';
const timestamp = decodeTime('01HN2K5QXYZ3NDEKTSV4RRFFQ6');
console.log(new Date(timestamp)); // 2024-01-15T08:20:47.123Z
ULID in Python
import ulid
# Generate a new ULID
u = ulid.new()
print(str(u)) # 01HN2K5QXYZ3NDEKTSV4RRFFQ6
# Extract timestamp
print(u.timestamp().datetime) # 2024-01-15 08:20:47.123000+00:00
# Parse an existing ULID string
parsed = ulid.from_str('01HN2K5QXYZ3NDEKTSV4RRFFQ6')
print(parsed.timestamp().int) # 1705312847123
ULID vs UUID Comparison
Format Example Sortable Size
----------- --------------------------------------- -------- ----
UUID v4 550e8400-e29b-41d4-a716-446655440000 No 128 bit
UUID v1 6ba7b810-9dad-11d1-80b4-00c04fd430c8 Yes* 128 bit
UUID v7 018e3a5f-1234-7abc-8def-000000000001 Yes 128 bit
ULID 01HN2K5QXYZ3NDEKTSV4RRFFQ6 Yes 128 bit
Sequential INT 1, 2, 3, 4 ... Yes 32/64 bit
* UUID v1 is sortable but exposes MAC address
ULID for Distributed Event Logging
In a microservices architecture, multiple services generate events simultaneously. Using ULIDs as event IDs allows merging logs from different services in chronological order:
Service A event: 01HN2K5QXYZ3NDEKTSV4RRFFQ6 (08:20:47.123)
Service B event: 01HN2K5QXYZ3NDEKTSV4RRFFQ9 (08:20:47.123)
Service C event: 01HN2K5QXYZ3NDEKTSV4RRFFQZ (08:20:47.124)
-- Merge and sort across services
SELECT service, event_id, payload
FROM events
ORDER BY event_id; -- chronological order, no timestamp column needed
B-Tree Index Performance
ULIDs improve write performance on indexed tables compared to UUID v4:
- UUID v4: random inserts scatter across the entire B-tree, causing page splits and fragmentation
- ULID: new records always append near the end of the index (monotonically increasing), minimizing fragmentation
- Result: significantly faster INSERT performance on high-write tables
-- Benchmark: 1 million inserts
-- UUID v4 primary key: ~45 seconds, high fragmentation
-- ULID primary key: ~12 seconds, minimal fragmentation
Common Use Cases
- Database primary keys where chronological ordering matters
- API resource identifiers (sortable by creation time)
- Distributed event IDs across microservices
- File names that sort by creation date
- Cache keys with built-in time ordering
- Audit log entry identifiers