KSUID generator
K-sortable identifiers: a 4-byte timestamp from the 2014 epoch plus 128 random bits, in 27 base62 characters.
What a KSUID is
A KSUID is 20 bytes: a 32-bit big-endian unsigned timestamp in seconds, followed by a 16-byte random payload. The text form is always 27 characters of base62. K-sortable means the text and binary forms both sort by generation time under an ordinary byte comparison, so running a file of them through sort produces a list in creation order with no special handling.
The alphabet is 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz: digits, then uppercase, then lowercase. That ordering is the entire reason the text form sorts, because it is ascending ASCII order, with 0-9 at 0x30 to 0x39 below A-Z at 0x41 to 0x5A below a-z at 0x61 to 0x7A. Base62 orderings that are not in ASCII order, which is most of them, produce strings that do not sort. Sqids and Hashids use such an ordering and are explicitly not sortable.
The 27 characters follow from the arithmetic. Twenty bytes is 160 bits, and 160 divided by log2(62) is 26.87, so 27 characters are needed. That leaves slack: 62^27 is 2.48e48 against 2^160 at 1.46e48, a ratio of about 1.7. Not every 27-character base62 string is a valid KSUID, which is why the reference implementation defines a maximum encoded value of aWgEPTl1tmebfsQzFP4bxwgy80V and rejects anything above it. It is the same class of overflow guard as ULID's 7ZZZ bound.
The 128-bit payload is the widest random component of any format on this site. The README puts it against UUIDv4 directly: 128 bits is 6 bits more than v4's 122, so the space is 64 times larger. The timestamp is on top of that, and two KSUIDs generated in different seconds cannot collide at all, which the README calls bonus entropy. At 128 random bits the 1 percent collision threshold is 2.61e18 identifiers, about 8.3e7 years at a thousand per second.
| Bytes | Field | Contents |
|---|---|---|
| 0-3 | Timestamp | 32-bit big-endian seconds since 2014-05-13T16:53:20Z |
| 4-19 | Payload | 128 bits from a CSPRNG |
| - | Text form | 20 bytes as one integer, base62, left-padded to 27 characters |
Second resolution is the real trade-off
The timestamp is four bytes of seconds. ULID and UUIDv7 both use 48 bits of milliseconds, and both define a way to order values generated inside a single tick. KSUID defines neither. Two KSUIDs created in the same second are separated only by their random payloads, so their relative order is arbitrary, and at any meaningful insert rate that describes almost all of them.
That matters differently depending on what you are sorting for. If the goal is index locality, second resolution is entirely adequate: every value written in the same second lands in the same neighbourhood of the B-tree, inserts stay at the right-hand edge, and page splits do not happen. The mechanism UUIDv7 exists to provide is intact.
If the goal is a total order, it is not adequate. Cursor pagination keyed on a KSUID will return rows in an unstable order inside each second, and a "latest first" list will shuffle within its most recent second on every query. Both need a tiebreaker column. An audit trail that must reconstruct the exact sequence of events cannot use a KSUID as the sequence, because the format does not carry one.
The reference README is honest about a second limitation, and it applies to every format in this family: "This is not a strong guarantee (an invariant) as it depends on wall clocks". A clock that steps backwards, from NTP correction or a virtual machine resuming from a snapshot, produces identifiers that sort before ones generated earlier. Nothing in the format detects it.
The README also warns about the fast random source: it "shouldn't be used in scenarios where uniqueness is important to security, as there is an increased chance the generated IDs can be predicted by an adversary". Every value on this page comes from crypto.getRandomValues, never from a seeded generator.
The epoch, and the seventeen-hour trap
KSUID counts seconds from a custom epoch rather than from 1970, which buys back the range that a 32-bit second counter would otherwise waste. The README describes that epoch as May 13th, 2014, and that is right to the day and wrong by most of one.
The constant in the reference source is epochStamp = 1400000000, a deliberately round 1.4e9. As a Unix timestamp that is 2014-05-13T16:53:20Z. Midnight on 2014-05-13 UTC would be 1399939200, a difference of 60,800 seconds, or about 16.9 hours. Anyone implementing KSUID from the README alone and choosing midnight will produce identifiers whose decoded timestamps are consistently about seventeen hours off, and whose sort order is still correct, which is exactly the kind of bug that survives a test suite.
The range follows from the same constant: 1400000000 plus 2^32 - 1 seconds is 2150-06-19T23:21:35Z. The README's claim of over 100 years of life is correct, at about 136 years from the epoch.
When to reach for a KSUID
KSUID is the right choice when you want time ordering and a wide random payload in a string that survives being pasted into things that were not designed for identifiers. It is purely alphanumeric with no delimiters, so nothing truncates it at a hyphen or tokenises it in the middle, which is a genuine problem with the 36-character UUID text form. Its own README notes that many projects adopted it just for that.
It is the wrong choice if you want a standard. There is no RFC, no database has a native type for it, and it is 27 characters against ULID's 26 and UUID's 36 while carrying 160 bits rather than 128. If you want time ordering and standards support, UUIDv7 is the answer. If you want time ordering and the shortest string, ULID is. KSUID wins on random payload width and on second-resolution ordering being enough for your case.
For a secret, the 128-bit payload is enough entropy on its own, but the first four bytes disclose the creation time to the second. If disclosure matters, use a Nano ID or raw random bytes, where nothing is derived from the clock.
| Format | Characters | Total bits | Random bits | Sorts by time | In a URL | Reveals creation time |
|---|---|---|---|---|---|---|
| KSUID | 27 | 160 | 128 | Yes, to the second | Yes, alphanumeric, no hyphens | Yes, to the second |
| ULID | 26 | 128 | 80 | Yes, to the millisecond | Yes, alphanumeric, no hyphens | Yes, to the millisecond |
| UUID v7 | 36 | 128 | 74 | Yes, to the millisecond | Yes, 32 hex digits and 4 hyphens | Yes, to the millisecond |
| UUID v4 | 36 | 128 | 122 | No | Yes, 32 hex digits and 4 hyphens | No |
| Nano ID (21) | 21 | 126 | 126 | No | Yes, no escaping. Can start with - or _. | No |
Doing this in your own code
Go
import "github.com/segmentio/ksuid"
id := ksuid.New() // CSPRNG payload
id.String() // 27 base62 characters
id.Time() // creation time, to the second
id.Payload() // the 16 random bytes
ksuid.Parse("0ujtsYcgvSTl8PAuAdqWYSMnLOv")
// Do not do this in anything security sensitive. It swaps the
// crypto/rand source for a seeded generator.
// ksuid.SetRand(ksuid.FastRander) On FastRander the README says it "shouldn't be used in scenarios where uniqueness is important to security, as there is an increased chance the generated IDs can be predicted by an adversary". The default source is crypto/rand.
JavaScript
// No dependency. 4 timestamp bytes plus 16 random bytes,
// rendered as a fixed 27-character base62 string.
const B62 =
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
const KSUID_EPOCH = 1400000000; // 2014-05-13T16:53:20Z
function ksuid() {
const b = new Uint8Array(20);
const ts = Math.floor(Date.now() / 1000) - KSUID_EPOCH;
b[0] = (ts >>> 24) & 0xff;
b[1] = (ts >>> 16) & 0xff;
b[2] = (ts >>> 8) & 0xff;
b[3] = ts & 0xff;
crypto.getRandomValues(b.subarray(4)); // 16 random bytes
let n = 0n;
for (const byte of b) n = (n << 8n) | BigInt(byte);
let out = "";
while (n > 0n) {
out = B62[Number(n % 62n)] + out;
n /= 62n;
}
return out.padStart(27, "0");
} The alphabet is digits, then uppercase, then lowercase. That ascending ASCII order is the only reason the text form sorts.
Python
import os, time
B62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
KSUID_EPOCH = 1400000000
def ksuid() -> str:
ts = int(time.time()) - KSUID_EPOCH
raw = ts.to_bytes(4, "big") + os.urandom(16)
n = int.from_bytes(raw, "big")
out = ""
while n:
n, r = divmod(n, 62)
out = B62[r] + out
return out.rjust(27, "0")
def ksuid_time(s: str) -> int:
n = 0
for ch in s:
n = n * 62 + B62.index(ch)
return (n >> 128) + KSUID_EPOCH Shell
# The reference CLI ships with the Go package.
go install github.com/segmentio/ksuid/cmd/ksuid@latest
ksuid # one KSUID
ksuid -n 10 # ten
ksuid -f inspect 0ujtsYcgvSTl8PAuAdqWYSMnLOv # decode time and payload Questions
Do KSUIDs sort reliably?
To the second, yes. Within a single second, no: the timestamp is identical and the remaining 128 bits are random, so the order of two KSUIDs from the same second is arbitrary. That is the format's main limitation against ULID and UUIDv7, both of which resolve to the millisecond and both of which define a monotonic mode. If you need a stable order inside a second, add a tiebreaker column.
KSUID or ULID?
ULID if ordering resolution matters, since it is millisecond-resolved and has a monotonic mode, and it is one character shorter. KSUID if you want the wider random payload: 128 bits against 80. Both are alphanumeric, both sort as text, and both publish their creation time. Neither is a standard, which is the argument for using UUIDv7 instead of either.
What is the KSUID epoch?
The reference implementation defines epochStamp = 1400000000, which is 2014-05-13T16:53:20Z. The README describes it as May 13th, 2014, which is correct to the day but not to midnight: midnight that day is 1399939200, about 16.9 hours earlier. Implementing from the README rather than the source produces timestamps that are consistently off by that amount.
Why are KSUIDs 27 characters?
Twenty bytes is 160 bits, and 160 divided by log2(62) is 26.87, so 27 base62 characters are needed to hold the value. The text encoding is fixed-length and left-padded with zeros, so every KSUID is 27 characters regardless of its numeric value.
Can I use a KSUID as an API key?
The 128-bit payload is enough entropy, and this page draws it from the operating system CSPRNG. The objection is disclosure rather than strength: the first four bytes are a readable creation time. Also avoid any implementation using the reference library's fast random source, which its own README says should not be used where uniqueness matters for security.
Does KSUID need coordination between machines?
No. There is no machine ID and no sequence counter, so any number of generators can produce KSUIDs independently. That is the stated reason its authors preferred it to Snowflake, which needs durable machine-ID assignment to avoid collisions in a 64-bit space.