# oklog/ulid: a Go ULID library with a real binary format

> The oklog/ulid package generates 128-bit IDs that sort by creation time, encode in 26 Crockford base32 characters, and ship with a CLI. It is the Go port to reach for when you want UUID compatibility without UUID's ordering problems.

**oklog/ulid** — Universally Unique Lexicographically Sortable Identifier (ULID) in Go

- Repository: https://github.com/oklog/ulid
- Stars: 5,049 · Forks: 192
- Language: Go
- License: Apache-2.0
- Published: 2026-09-23 · Updated: 2026-09-23 · Language: en
- Canonical page: https://hysenlabs.com/projects/oklog-ulid

## What oklog/ulid solves, and for whom

The README frames the problem as a set of UUID shortcomings. UUID v1 and v2 need a stable MAC address, which is impractical in many environments. UUID v3 and v5 need a unique seed and produce randomly distributed IDs, which the README says can cause fragmentation in many data structures. UUID v4 carries nothing but randomness, with the same fragmentation concern. On top of that, a UUID string is 36 characters and a GUID is not the most character-efficient way to encode 128 bits.

A ULID keeps 128 bits but splits them differently: 48 bits of millisecond timestamp and 80 bits of entropy, canonically written as 26 Crockford base32 characters. The README lists the consequences: lexicographically sortable, case insensitive, URL safe, no special characters, and compatible with UUID and GUID. The stated headroom is 1,208,925,819,614,629,174,706,176 unique ULIDs per millisecond, and the 48-bit timestamp does not run out until the year 10889.

The audience is Go developers who need an ID that doubles as a creation-time key. If you are storing rows in a database and want them to insert in roughly chronological order, or you are building a log or event stream where sorting by ID should approximate sorting by time, that is the case this library targets. The README is blunt about the inverse: if you do not care about time-based ordering, there is no reason to use ULIDs, and UUIDs are easier, faster and smaller.

## How the Go implementation is put together

The package models a ULID as two inputs. Timestamps are uint64 values holding a Unix time in milliseconds, produced either by passing a time.Time to ulid.Timestamp or by calling time.Time.UnixMilli and converting to uint64. Entropy is drawn from any io.Reader you supply. That split is deliberate: the README says it allows flexibility in choosing trade-offs, and also that it can be confusing to newcomers. Both statements are fair. Passing an io.Reader means the library never decides how secure or how fast your IDs are.

For the common case there is ulid.Make, which calls time.Now and uses a process-global entropy source that the README describes as pseudo-random and monotonic, backed by LockedMonotonicReader. That is the one-liner most callers want. The advanced path is ulid.New(ms, entropy), where you control both halves.

Monotonicity deserves care. ULIDs are automatically monotonic, but only to millisecond precision, because IDs generated inside the same millisecond are ordered by their random component and are therefore unordered by default. ulid.MonotonicEntropy and ulid.LockedMonotonicEntropy exist to make IDs monotonic within a single millisecond, with caveats that the README defers to the package documentation rather than spelling out. If strict within-millisecond ordering matters to you, read that documentation before assuming the default gives it to you.

On the wire, the 16 octets use network byte order, most significant byte first, with the 48-bit timestamp in the leading bytes and the 80 bits of entropy behind it. That layout is what makes byte comparison equal time comparison.

## Installing oklog/ulid and generating your first ID

The package requires Go modules. The README gives a single install command:

```bash
go get github.com/oklog/ulid/v2
```

After that, the shortest working program uses the helper function. The README shows it printing a value like 01G65Z755AFWAKHE12NY0CQ9FH:

```go
fmt.Println(ulid.Make())
// 01G65Z755AFWAKHE12NY0CQ9FH
```

If you need control over the timestamp and the entropy source, use ulid.New. The README's example builds a math/rand source seeded from the current nanosecond clock:

```go
entropy := rand.New(rand.NewSource(time.Now().UnixNano()))
ms := ulid.Timestamp(time.Now())
fmt.Println(ulid.New(ms, entropy))
```

That example comes with an explicit warning: math/rand.Rand is not safe for concurrent use by multiple goroutines. The README points at x/exp/rand's LockedSource as an alternative, says security-sensitive code should use crypto/rand, and notes that performance-sensitive code can avoid synchronization by giving each goroutine its own entropy source, at the cost of weaker randomness guarantees and no monotonicity within a millisecond. It also mentions pooling entropy sources with sync.Pool as a common optimization.

The repository ships a command line tool as well. Install it with:

```bash
go install github.com/oklog/ulid/v2/cmd/ulid@latest
```

Running ulid with no arguments prints a fresh identifier, and the README's examples include ulid -z to fix entropy to all zeroes and ulid 01D78XZ44G0000000000000000 to decode a value back to a time. The -f flag accepts default, rfc3339, unix or ms, and -l switches parsing output to local time instead of UTC.

## Where the entropy design bites you

The io.Reader parameter is the library's main trade-off, and it is a real one. A newcomer who writes ulid.New(ms, rand.New(rand.NewSource(seed))) and calls it from several goroutines has a data race on the math/rand source, and the README says so directly. The convenience function ulid.Make sidesteps this with a locked, monotonic, process-global reader, but that reader is pseudo-random, not cryptographically secure. If your IDs end up in a context where unpredictability matters, ulid.Make is the wrong call and crypto/rand is the right one.

The second sharp edge is monotonicity. The README states that ULIDs generated within the same millisecond are ordered by their random component and are therefore unordered by default. Teams that assume a ULID column gives them insertion order at sub-millisecond resolution will be surprised. The monotonic entropy readers exist to fix this, but the README explicitly says they come with caveats and sends you to the package documentation instead of listing them, which is the thinnest part of an otherwise careful README.

Third, the README's own closing advice is worth repeating: if time-based ordering is not something you need, ULIDs buy you nothing over UUIDs. They are longer to type than a random integer, they carry a timestamp you may not want to leak, and they force you to think about an entropy source. That is a genuine cost, not a footnote.

## oklog/ulid against UUID and other ULID ports

The obvious alternative is a UUID library, and the difference is not cosmetic. UUID v4 gives you 122 random bits and no ordering; a ULID gives you 48 bits of millisecond time plus 80 random bits, and byte order matches time order. The README's framing is that UUID v4's pure randomness causes fragmentation in data structures, which is exactly the B-tree and index behaviour you notice when random keys are inserted into a sorted structure. UUID v7, which the search data pairs against ULID, is the closer comparison in spirit because it also puts a timestamp first; the README does not describe v7's layout, so treat that comparison as one to verify against the UUID specification rather than against this README.

Among ULID implementations, oklog/ulid is the Go port of ulid/javascript, and the README notes it adds a binary format implementation. That matters if you want to store 16 bytes rather than a 26-character string. Ports exist in other languages, and the search data shows people looking for Python, JavaScript, Rust and npm variants, but this repository is the Go one and the others are separate projects with their own APIs. If your service is not written in Go, this package is not the tool, even though the identifier format is the same.

## Maintenance, licensing and upgrade cost

The repository is not archived, and the most recent push recorded is 2026-07-23, which is the same date as the v2.1.2 release. The release history is sparse: v2.1.0 landed in 2022, v2.1.1 in 2025, and v2.1.2 in 2026. That cadence suggests a library that is finished rather than one under constant change, and the API surface is small enough that this is a reasonable posture. Do not read the gaps as abandonment, but do not expect frequent feature work either.

The module path is versioned: github.com/oklog/ulid/v2. The go.mod declares go 1.15 and pulls in github.com/pborman/getopt for the command line tool. That dependency is old, and it is confined to cmd/, so a library-only consumer carries it in the module graph without using it at runtime.

The licence is Apache-2.0. For most users that is a permissive licence with an explicit patent grant, and it is compatible with typical commercial use, but the terms are what they are and this is not legal advice. If your organisation has a licence allowlist, check Apache-2.0 against it before you add the dependency. Upgrading from v2.1.1 to v2.1.2 is a patch bump, and the README gives no breaking-change notes; the CHANGELOG.md at the repository root is where that would be recorded.

## Conclusion

Adopt oklog/ulid if you are writing Go and want time-sortable 128-bit IDs that still fit in a UUID column, and you accept that the entropy source is your responsibility. Do not adopt it if you only want opaque random IDs, or if you need monotonicity guarantees across processes rather than within one. Before committing, check which entropy path your code takes, since ulid.Make uses a process-global pseudo-random monotonic reader while ulid.New uses whatever io.Reader you pass, and the two behave differently under concurrency.

## FAQ

### What is a ULID used for?

It is a 128-bit identifier that combines a millisecond timestamp with random entropy, so it sorts lexicographically by creation time while remaining compatible with UUID and GUID. The README suggests using it when time-based ordering of generated IDs matters, such as keys that should insert in roughly chronological order.

### Is a ULID better than a UUID?

It depends on whether you need ordering. The README lists UUID v4's pure randomness as a cause of fragmentation in data structures and presents ULIDs as lexicographically sortable, but it also states that if you do not care about time-based ordering there is no reason to use ULIDs and UUIDs are easier, faster and smaller.

### Can you give an example of an oklog/ulid value?

The README shows fmt.Println(ulid.Make()) producing 01G65Z755AFWAKHE12NY0CQ9FH, and the command line examples include 01D78XYFJ1PRM1WPBCBT3VHMNV from running ulid with no arguments.

### What does UUID stand for?

The README only uses the term UUID/GUID and never expands the acronym, so it does not answer this. What it does say is that ULIDs are compatible with UUID and GUID, and that a ULID is 26 characters against a UUID's 36.

## Sources

- [Issues](https://github.com/oklog/ulid/issues)
- [License: Apache-2.0](https://github.com/oklog/ulid/blob/main/LICENSE)
- [oklog/ulid on GitHub](https://github.com/oklog/ulid)
- [README](https://github.com/oklog/ulid/blob/main/README.md)
- [Releases](https://github.com/oklog/ulid/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/oklog-ulid
