What is in a ULID
A ULID (Universally Unique Lexicographically Sortable Identifier) is 128 bits, like a GUID: the first 48 bits are the time in Unix milliseconds, the other 80 are random. It is written as 26 characters of Crockford's base32 (digits and letters without I, L, O and U), so 01ARZ3NDEKTSV4RRFFQ69G5FAV is the time 01ARZ3NDEK followed by the randomness TSV4RRFFQ69G5FAV.
first 10 characters: timestamp (48 bits)last 16 characters: randomness (80 bits)
Because the time comes first, ULIDs sort by creation time as strings, as bytes and in Ulid.CompareTo, which keeps database indexes from fragmenting the way random GUIDs do. The 48-bit timestamp lasts until the year 10889.
ULIDs in .NET
.NET has no built-in ULID type. The Ulid package by Cysharp is the common choice: a struct with the same 16 bytes as a Guid, fast parsing and formatting, and converters for System.Text.Json.
// dotnet add package Ulid
Ulid id = Ulid.NewUlid(); // now + 80 random bits
string text = id.ToString(); // 26 characters, sorts by time
Guid guid = id.ToGuid(); // for a uniqueidentifier or uuid column
Ulid back = new Ulid(guid); // and back: back == id
// Ulid.NewUlid() does not count within a millisecond: two IDs made in the
// same millisecond sort randomly. Use the MonotonicUlid class below if order matters.
ULID to GUID: the byte order trap
A ULID and a GUID are both 16 bytes, so converting is free, but there are two ways to do it and they give different GUIDs:
| Code | GUID for 01ARZ3NDEKTSV4RRFFQ69G5FAV |
|---|---|
ulid.ToGuid() | 01563e3a-b5d3-d676-4c61-efb99302bd5b |
new Guid(ulid.ToByteArray()) | 3a3e5601-d3b5-76d6-4c61-efb99302bd5b |
ToGuid() keeps the bytes in order, so the GUID shows the same hex as the ULID and still sorts by time as a string and in PostgreSQL's uuid. new Guid(byte[]) reads the first three groups little-endian and reverses them. Pick one and use it everywhere: new Ulid(guid) undoes ToGuid(), not the other one. Paste a GUID above to see both readings.
SQL Server is a separate problem: it compares uniqueidentifier values starting with the last six bytes, which in a ULID are random, so ULIDs stored as uniqueidentifier do not sort by time there. Store them as binary(16) or char(26) if SQL Server's order matters; the GUID tool shows SQL Server's order.
ULID vs GUID version 7
Guid.CreateVersion7() (.NET 9 and later) uses the same idea: 48 bits of Unix milliseconds first, then random bits. The differences are small: a version 7 GUID gives 6 bits to the version and variant, so it has 74 random bits instead of 80, it is written as 36 hex characters instead of 26, and every database and serializer already knows the type. Read as a ULID, a version 7 GUID even has the right time:
// Guid.CreateVersion7() (.NET 9+) also starts with 48 bits of Unix milliseconds,
// so a version 7 GUID read as a ULID has the right time.
Guid v7 = Guid.CreateVersion7();
Ulid asUlid = new Ulid(v7);
Console.WriteLine(asUlid.Time); // when v7 was created (UTC)
If you are on .NET 9 or later and do not need the short string, a version 7 GUID does the same job without a package. If you need short, URL-friendly, case-insensitive IDs, or IDs that other systems already produce as ULIDs, use ULIDs.
Monotonic ULIDs
Ulid.NewUlid() draws new random bits every time, so two ULIDs from the same millisecond sort in random order: in a tight loop we measured about half of consecutive values out of order. The ULID spec's monotonic mode fixes that: within one millisecond, the next ULID is the previous one plus 1. The generator above uses it when "Monotonic" is ticked. Cysharp's package does not have it, so here is a small class:
using System.Security.Cryptography;
// The ULID spec's monotonic mode for Cysharp's Ulid: in the same millisecond,
// the next ULID is the previous one plus 1, so IDs sort in creation order.
public sealed class MonotonicUlid
{
private readonly object _lock = new();
private readonly byte[] _random = new byte[10];
private long _lastMs = -1;
public Ulid Next()
{
lock (_lock)
{
long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
if (now > _lastMs)
{
_lastMs = now;
RandomNumberGenerator.Fill(_random);
}
else
{
// Same millisecond, or the clock went back: add 1 to the 80 random bits.
int i = _random.Length - 1;
while (i >= 0 && ++_random[i] == 0) i--;
if (i < 0) throw new OverflowException("More than 2^80 ULIDs in one millisecond.");
}
return Ulid.NewUlid(DateTimeOffset.FromUnixTimeMilliseconds(_lastMs), _random);
}
}
}
Use one instance per process (a singleton in dependency injection). Monotonic ULIDs are easier to guess, since the next one is the last plus 1; do not use them as secrets.
What Ulid.Parse does not check
Cysharp's Ulid.Parse checks only the length. Letters outside the alphabet (I, L, O, U, or punctuation) are read as garbage bits and a first character above 7 loses its top bits, so 01ARZ3NDEKTSV4RRFFQ69G5FAI parses without an error as 01ARZ3NDEKTSV4RRFFQ69G5FFZ, a different ID. Non-ASCII characters throw IndexOutOfRangeException rather than FormatException, and Ulid.TryParse returns true in all the silent cases. Validate ULIDs from outside (the decoder above follows the spec) before you parse them. ulid.Time also throws for timestamps after the year 9999, which a random 26-character string can easily have.
FAQ
What is a ULID?
A 128-bit identifier made of a 48-bit Unix millisecond timestamp and 80 random bits, written as 26 characters of Crockford base32. ULIDs sort by creation time.
How do I generate a ULID in C#?
Install the Ulid NuGet package and call Ulid.NewUlid(). ToString() gives the 26-character form and ToGuid() a Guid with the same bytes.
How do I convert a ULID to a GUID?
With the Ulid package, ulid.ToGuid(), and new Ulid(guid) to go back. The GUID shows the same hex as the ULID. new Guid(ulid.ToByteArray()) gives a different GUID with the first three groups reversed.
Is a ULID better than a GUID version 7?
They are nearly the same: both start with 48 bits of Unix milliseconds. ULIDs have 80 random bits and a 26-character string; version 7 GUIDs have 74 random bits, a 36-character string and built-in support in .NET 9+ (Guid.CreateVersion7).
Are ULIDs sequential in SQL Server?
Not as uniqueidentifier: SQL Server compares the last six bytes first, and those are random in a ULID. They are sequential as binary(16), as strings, and as PostgreSQL uuid.
Can a ULID contain the letters I, L, O or U?
No. Crockford base32 leaves them out. Cysharp's Ulid.Parse still accepts them without an error and returns a different ID, so validate input first.