Skip to main content

ULID

Generate a ULID string or write the bytes into a buffer. ULID (Universally Unique Lexicographically Sortable Identifier) is a 128-bit identifier with millisecond timestamp precision and 80 bits of randomness. ULIDs are URL-safe, use Crockford’s Base32 encoding, and sort lexicographically by creation time.

Import

import { ulid } from 'uniku/ulid'

Function Signature

type Ulid = {
  (): string
  <TBuf extends Uint8Array = Uint8Array>(options: UlidOptions | undefined, buf: TBuf, offset?: number): TBuf
  (options?: UlidOptions, buf?: undefined, offset?: number): string
  toBytes(id: string): Uint8Array
  fromBytes(bytes: Uint8Array): string
  timestamp(id: string): number
  isValid(id: unknown): id is string
  NIL: string
  MAX: string
}

Basic Usage

const id = ulid()
// => "01HW9T2W9W9YJ3JZ1H4P4M2T8Q"

Options

options
UlidOptions
Configuration options for ULID generation

Buffer Mode

Write ULID bytes directly to a buffer at a specific offset:
options
UlidOptions | undefined
Configuration options (can be undefined)
buf
Uint8Array
required
Target buffer to write the ULID bytes into (16 bytes required)
offset
number
default:"0"
Byte offset in the buffer where ULID should be written
return
Uint8Array
Returns the same buffer instance that was passed in
const buffer = new Uint8Array(32)
ulid(undefined, buffer, 0)  // writes 16 bytes at offset 0
ulid(undefined, buffer, 16) // writes 16 bytes at offset 16

Static Methods

toBytes()

Convert a ULID string to a byte array.
id
string
required
ULID string in Crockford Base32 format (e.g., “01HW9T2W9W9YJ3JZ1H4P4M2T8Q”)
return
Uint8Array
16-byte array representation of the ULID
const bytes = ulid.toBytes("01HW9T2W9W9YJ3JZ1H4P4M2T8Q")
// => Uint8Array(16)

fromBytes()

Convert a byte array to a ULID string.
bytes
Uint8Array
required
16-byte array representing a ULID
return
string
ULID string in Crockford Base32 format (26 characters)
const bytes = new Uint8Array(16)
const id = ulid.fromBytes(bytes)
// => "00000000000000000000000000"

timestamp()

Extract the embedded timestamp from a ULID string.
id
string
required
ULID string to extract timestamp from
return
number
Unix timestamp in milliseconds
const id = ulid()
const ts = ulid.timestamp(id)

console.log(new Date(ts))
// => Date object representing when the ULID was created

console.log(Date.now() - ts)
// => Milliseconds elapsed since ULID creation
Note: Only the first 10 characters of the ULID encode the timestamp.

isValid()

Validate whether a value is a valid ULID string (type guard).
id
unknown
required
Value to validate
return
id is string
Type predicate: true if the value is a valid ULID string, false otherwise
const maybeUlid: unknown = getUserInput()

if (ulid.isValid(maybeUlid)) {
  // TypeScript now knows maybeUlid is a string
  const timestamp = ulid.timestamp(maybeUlid)
}
Validation rules:
  • Must be a string
  • Must be exactly 26 characters
  • Must match the pattern: /^[0-7][0-9A-HJKMNP-TV-Z]{25}$/i
  • First character must be 0-7 (prevents timestamp overflow)
  • Rest must be valid Crockford Base32 characters (excludes I, L, O, U)

Constants

NIL

The nil ULID (all zeros).
ulid.NIL
// => "00000000000000000000000000"

MAX

The max ULID (maximum valid value).
ulid.MAX
// => "7ZZZZZZZZZZZZZZZZZZZZZZZZZ"

Monotonic Ordering

ULID maintains module-level state to ensure IDs are monotonically increasing even when generated within the same millisecond:
// All called within the same millisecond
const id1 = ulid() // random portion = new random value
const id2 = ulid() // random portion = incremented
const id3 = ulid() // random portion = incremented again

// Guaranteed to be in lexicographic order
console.log(id1 < id2 && id2 < id3) // true
Important: This module-level state persists across calls:
  • In serverless/edge functions with warm starts, state persists between invocations
  • For isolated state, pass explicit msecs and random via options
  • Tests should mock Date.now() or provide explicit options for deterministic behavior
Cloudflare Workers note: Cloudflare Workers “freeze” time during request handling to prevent side-channel attacks. This means all ULIDs generated within a single request will have the same timestamp, and monotonic ordering will rely entirely on incrementing the random portion.

Advanced Usage

Deterministic Generation (Testing)

Provide explicit options for reproducible ULIDs:
const id = ulid({
  msecs: 1702387456789,
  random: new Uint8Array(16)
})
// Always generates the same ULID for the same inputs

Custom Timestamp

Generate ULIDs with a specific timestamp:
const pastTime = Date.now() - 86400000 // 24 hours ago
const id = ulid({ msecs: pastTime })

console.log(ulid.timestamp(id) === pastTime) // true

Custom Random Source

Provide your own random bytes:
const myRandomBytes = new Uint8Array(16)
crypto.getRandomValues(myRandomBytes)

const id = ulid({ random: myRandomBytes })

Structure

ULID format (128 bits / 16 bytes):
Bytes 0-5:   48-bit timestamp (milliseconds since Unix epoch)
Bytes 6-15:  80 bits of randomness
Encoded as 26 characters in Crockford Base32:
Characters 0-9:   Timestamp (10 characters)
Characters 10-25: Randomness (16 characters)
Crockford Base32 Alphabet:
  • 0123456789ABCDEFGHJKMNPQRSTVWXYZ (case-insensitive)
  • Excludes: I, L, O, U (to avoid confusion with 1, 1, 0, V)

Errors

InvalidInputError
Error
Thrown when random bytes array is too short (< 10 bytes)Code: ULID_RANDOM_BYTES_TOO_SHORT
BufferError
Error
Thrown when buffer offset is out of boundsCode: ULID_BUFFER_OUT_OF_BOUNDS
ParseError
Error
Thrown when parsing an invalid ULID string format
import { InvalidInputError, BufferError, ParseError } from 'uniku/ulid'

Type Definitions

export type UlidOptions = {
  /**
   * 16 bytes of random data to use for ULID generation.
   * Only the first 10 bytes are used.
   */
  random?: Uint8Array
  /**
   * Timestamp in milliseconds since Unix epoch.
   * Defaults to Date.now().
   */
  msecs?: number
}

export type Ulid = {
  (): string
  <TBuf extends Uint8Array = Uint8Array>(options: UlidOptions | undefined, buf: TBuf, offset?: number): TBuf
  (options?: UlidOptions, buf?: undefined, offset?: number): string
  toBytes(id: string): Uint8Array
  fromBytes(bytes: Uint8Array): string
  timestamp(id: string): number
  isValid(id: unknown): id is string
  /** The nil ULID (all zeros) */
  NIL: string
  /** The max ULID (maximum valid value) */
  MAX: string
}

Performance

The string generation path is optimized to avoid buffer allocation:
  • Direct encoding from timestamp and random bytes to Crockford Base32
  • No intermediate buffer creation
  • Efficient monotonic state management

Comparison with UUID v7

FeatureULIDUUID v7
Format26-char Base3236-char hex with hyphens
Timestamp precisionMillisecond (48 bits)Millisecond (48 bits)
Randomness80 bits74 bits (31-bit seq + 43 random)
URL-safeYes (no special chars)No (contains hyphens)
Case-sensitiveNoNo
Lexicographic sortingYesYes
Binary size16 bytes16 bytes

See Also

  • UUID v7 - Time-ordered UUID with standard format
  • KSUID - Time-ordered with second precision
  • Nanoid - Compact URL-friendly IDs without time ordering

Build docs developers (and LLMs) love