kid

package module
v1.3.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 7, 2026 License: MIT Imports: 8 Imported by: 0

README

GitHub go.mod Go versiongodocTestCoverage

kid

Package kid (K-sortable ID) provides a goroutine-safe generator of short (10 byte binary, 16 bytes when base32 encoded), url-safe, k-sortable unique IDs.

The 10-byte binary representation of an ID is composed of:

  • 6-byte value representing Unix time in milliseconds
  • 2-byte sequence, and,
  • 2-byte random value.

IDs encode (base32) as 16-byte url-friendly strings that look like:

06bqj05bhh2lcbdb

kid.ID features

  • Size: 10 bytes as binary, 16 bytes if stored/transported as an encoded string.
  • Timestamp + sequence is guaranteed to be unique and monotonically increasing for each call to New(), even if the wall clock steps backwards.
  • 2 bytes of trailing randomness to avoid counter-based attacks, drawn from math/rand/v2's per-goroutine ChaCha8 generator, seeded by the Go runtime from OS entropy.
  • K-orderable in both binary and base32 encoded representations; the encoding alphabet is in ascending ASCII order, so encoded strings sort identically to the underlying bytes.
  • Lock-free, allocation-free ID generation that scales with cores; no mutex in the New() path.
  • URL-friendly custom encoding without the vowels a, i, o, and u.
  • Automatic (un)/marshalling for SQL and JSON.
  • cmd/kid tool for ID generation and introspection.

Requires Go 1.23+; no newer version is needed for performance — benchmarks published here were produced with Go 1.26. kid has no dependencies outside the standard library.

Security note: an ID carries only 16 bits of randomness alongside values derived from the clock; IDs are predictable by design. Do not use kid IDs where unguessability matters, such as session tokens, API keys, or password reset codes.

Example usage

func main() {
	id := kid.New()
	fmt.Printf("%s %03v\n", id, id[:])
	// Example output: 06bq7xhnr03mlz6r [001 149 115 246 021 192 007 073 252 216]

	id, err := kid.FromString("06bq7xhnr03mlz6r")
	if err != nil {
		// handle the error
	}
	fmt.Printf("%s %03v\n", id, id[:])
	// Output: 06bq7xhnr03mlz6r [001 149 115 246 021 192 007 073 252 216]
}

Acknowledgments

  • While the ID payload differs greatly, the API and much of this package borrows heavily from github.com/rs/xid, a zero-configuration globally-unique ID generator.

  • The timestamp+sequence encoding is derived from the github.com/google/uuid getV7Time() algorithm; kid replaces its mutex protection with a lock-free atomic claim.

Third-party copyright notices and license texts are reproduced in NOTICES.

Uniqueness

Each call to kid.New() is guaranteed to return a unique ID with a timestamp+sequence greater than any previous call, even across goroutines and even if the system clock steps backwards.

To satisfy whether kid.IDs are unique, run eval/uniqcheck/main.go, which generates IDs concurrently without locking, then verifies post-hoc that no timestamp+sequence pair ever repeats and that each goroutine observed strictly increasing IDs:

$ go run eval/uniqcheck/main.go -count 2000000 -goroutines 20
# example output:
uniqcheck: generating 2,000,000 IDs on each of 20 goroutines...
Total IDs: 40,000,000  ts+seq dupes: 0  full-ID dupes: 0  ordering violations: 0

Or, at the command line, produce IDs and use OS utilities to check (single-threaded):

$ go install github.com/mwyvr/kid/cmd/kid@latest
$ kid -c 2000000 | sort | uniq -d
// None output
Uniqueness scope

The guarantee is per process: the timestamp+sequence pair is claimed from a single atomic value, so two calls within one process can no more collide than two atomic increments can return the same number — uniqueness is structural, not probabilistic, and does not depend on the random bytes. Across processes or machines there is no coordination (kid deliberately omits xid's machine ID and PID bytes in exchange for shortness): two processes that derive the same timestamp+sequence in the same ~256ns window are separated only by the two random bytes, a 1-in-65,536 chance per such coincidence. If you need cross-machine uniqueness at high sustained rates, use a coordinated or longer ID (xid, uuid).

Capacity and timestamp drift

An ID's uniqueness is carried entirely by the timestamp+sequence pair: 4,096 sequence slots per millisecond, or a sustained capacity of ~4.1 million IDs per second, per process. (The random bytes multiply the namespace, not the capacity.) A clock reading derives the sequence from the fractional nanoseconds at 256ns granularity, yielding values 0-3906; the remaining slots up to 4095 are headroom consumed under load before the sequence borrows into the next millisecond.

Generation bursts exceeding ~4.1M IDs/s — trivially reached by benchmarks, rarely by applications — push the internal clock ahead of real time: each second of full-rate generation consumes several seconds of timestamp space, and the embedded timestamps lead the wall clock until generation slows and real time catches up. Nothing about uniqueness or ordering is affected; IDs remain strictly k-sortable in generation order at any rate. The practical guidance: kid is a good fit for systems that treat the embedded time as approximate metadata, and a poor fit for systems that require ID timestamps to be exact wall-clock instants under extreme generation rates.

For the doubtful

The race detector verifies concurrent generation analytically (ordering and ts+seq uniqueness across goroutines):

go test -race -run TestNewUniqueParallel -count=20 .

Fuzzing hammers the decode paths (each target runs separately; failing inputs, should one ever appear, land in testdata/fuzz/ and become permanent regression tests):

go test -fuzz '^FuzzFromString$'    -fuzztime 60s .
go test -fuzz '^FuzzUnmarshalJSON$' -fuzztime 60s .
go test -fuzz '^FuzzFromBytes$'     -fuzztime 60s .

And uniqcheck brute-forces the uniqueness and ordering guarantees under real contention — one large run, then a burst loop whose oversubscribed goroutines explore scheduler interleavings a single run never hits:

go run ./eval/uniqcheck -count 4000000 -goroutines 32
for i in $(seq 1 20); do go run ./eval/uniqcheck -count 500000 -goroutines 64 || break; done

uniqcheck holds every generated ID in memory (~10 bytes each) for post-hoc verification; size count x goroutines to available RAM.

CLI

Package kid also provides a tool for id generation and inspection:

$ kid -version
kid v1.3.0 (go1.26.3 linux/amd64)

$ kid
06bpwm8x107evvh9

$ kid -c 2
06bpwm3hkm371gz4
06bpwm3hkm3d5ezr

# produce 4 and inspect
kid $(kid -c 4)
06bpwlvhb86bypp7 ts:1741312454738 seq:3247 rnd:23239 2025-03-07 01:54:14.738 +0000 UTC ID{  0x1, 0x95, 0x6e, 0x4f, 0x70, 0x52,  0xc, 0xaf, 0x5a, 0xc7 }
06bpwlvhb86gcdw6 ts:1741312454738 seq:3317 rnd:45958 2025-03-07 01:54:14.738 +0000 UTC ID{  0x1, 0x95, 0x6e, 0x4f, 0x70, 0x52,  0xc, 0xf5, 0xb3, 0x86 }
06bpwlvhb86gkmks ts:1741312454738 seq:3320 rnd:53817 2025-03-07 01:54:14.738 +0000 UTC ID{  0x1, 0x95, 0x6e, 0x4f, 0x70, 0x52,  0xc, 0xf8, 0xd2, 0x39 }
06bpwlvhb86gmb73 ts:1741312454738 seq:3322 rnd:10467 2025-03-07 01:54:14.738 +0000 UTC ID{  0x1, 0x95, 0x6e, 0x4f, 0x70, 0x52,  0xc, 0xfa, 0x28, 0xe3 }

Change Log

v1.3.0: lock-free New(), full-width Compare, hardened decode paths

  • New() is lock-free and allocation-free: atomic CAS + wait-free increment replaces the mutex; trailing bytes from math/rand/v2 (ChaCha8) replace per-call crypto/rand
  • Compare/Sort consider all 10 bytes, consistent with ==
  • UnmarshalJSON rejects non-string JSON values (bug fix)
  • Scan accepts the 10-byte binary form
  • New tests: clock regression, sequence borrow, parallel CAS stress, three fuzz targets; eval/uniqcheck rewritten; CI runs -race on 1.23.x and stable across linux/macos/windows"

main:

  • Drop minimum supported Go version to 1.23, thanks to heads up from @sergeevabc.

v1.2.0 released:

  • 2025-03-06 Forked rid in favour of kid for true k-sortability, requiring a new ID payload, now expected to remain static. Improved code coverage and documentation.

Contributing

Contributions are welcome.

Package Comparisons

kid was born out of a desire for a short, k-sortable unique ID where global uniqueness or inter-process ID generation coordination is not required.

A comparison of various Go ID generators:

Package BLen ELen K-Sort Encoded ID and Next Unique Components
mwyvr/kid 10 16 true 06bwz2qyzm14d070
06bwz2qyzm14fnte
06bwz2qyzm14hxmf
06bwz2qyzm14kdl1
unique (ts(ms) + sequence) + chacha8 rand 6 byte ts(millisecond) : 2 byte sequence : 2 byte random
rs/xid 12 20 true cvhjc0tq9fa75iaa3d00
cvhjc0tq9fa75iaa3d0g
cvhjc0tq9fa75iaa3d10
cvhjc0tq9fa75iaa3d1g
ts(sec) + machineID + pid + counter 4 byte ts(sec) : 2 byte mach ID : 2 byte pid : 3 byte monotonic counter
segmentio/ksuid 20 27 true 2upRtyliBRn6UnfS2RsdkEIhqbg
2upRu1TTpojt5KQDykjTjreGXGE
2upRu0IZ0RbjFMSS1lb0Io3aQ8A
2upRu2AjlZoy3rnU6MJdqSuDs1H
ts + crypto/rand 4 byte ts(sec) : 16 byte random
google/uuid V4 16 36 false f03fed10-c632-4d06-95b5-6783796e6aaa
1c6c044b-66db-44e8-ac45-0e4e358dcc1f
9b26d7cd-d85d-4696-beec-0483d6446e7f
f2cf8c26-4e3c-4612-865c-2270883456fd
crypt/rand v4: 122 bits random; 6 bits embedding version & variant
google/uuid V7 16 36 true 0195cf8a-fefd-725a-8655-0910a814aa54
0195cf8a-fefd-725b-b7ee-b178436e1aa0
0195cf8a-fefd-725e-b663-bc0ae6ee45f9
0195cf8a-fefd-725f-8936-57d2937b3ecc
ts(ms) + crypt/rand v7: 16 bytes : 48 bits time, 12 bits sequence, 6 bits version/variant, 62 bits random
chilts/sid 16 23 true 1WlBXfeKc31-1L10__76tcP
1WlBXfeKcCQ-4_us_ZM7ZtY
1WlBXfeKcMr-0HIZhTn1J_0
1WlBXfeKcUN-4AYyBPKLA5P
ts + math/rand 8 byte ts(nanosecond) 8 byte random
matoous/go-nanoid/v2 21 21 false iaGKMTIcslXAPNkbIp4ho
hM2E2H2y56NtaljmWCfNs
SpTDFWVxLW_Rrk_S93A87
kPxkTozTenuSfwAO0s9rb
ts + crypto/rand 21 byte rand (adjustable)
sony/sonyflake 16 29 true GU2TSMZXGYYTCNZVGYYDANBZHEZTA
GU2TSMZXGYYTCNZVGYYDCMJVGQ3DM
GU2TSMZXGYYTCNZVGYYDCOBRGAYDE
GU2TSMZXGYYTCNZVGYYDENBWGUZTQ
ts + counter 39 bit ts(10msec) 8 bit seq, 16 bit mach id
oklog/ulid 16 26 true 01JQ7RNZQXCZ605958V4E9XMF0
01JQ7RNZQXK35FDQBEQM86VMEF
01JQ7RNZQXJV8JBXKFE1VVPJJT
01JQ7RNZQX1GJ4EQSG1KC3RY8N
ts + user-definable rand src 6 byte ts(ms) : 10 byte monotonic counter random init per ts(ms)
kjk/betterguid 17 20 true -OMEXjvxqWZMbCF4xNP6
-OMEXjvxqWZMbCF4xNP7
-OMEXjvxqWZMbCF4xNP8
-OMEXjvxqWZMbCF4xNP9
ts + rand-init counter 8 byte ts(ms) : 9 byte counter random init per ts(ms)

Package Benchmarks

kid's New() is lock-free and allocation-free, and generation scales with cores; against rs/xid — the fastest comparable generator — it is effectively a draw on amd64 and within ~20% on Apple silicon, while additionally guaranteeing strictly increasing timestamp+sequence ordering.

Benchmarked with Go 1.26 (go test -cpu 1,2,4,8,16,32 -test.benchmem -bench . in eval/bench). On Linux, set the scaling governor to performance; on macOS laptops, use High Power mode:

echo "performance" | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor
goos: linux
goarch: amd64
cpu: Intel(R) Core(TM) i9-14900K
BenchmarkKid                    36166317                28.90 ns/op            0 B/op          0 allocs/op
BenchmarkKid-2                  35779724                30.58 ns/op            0 B/op          0 allocs/op
BenchmarkKid-4                  38237456                32.65 ns/op            0 B/op          0 allocs/op
BenchmarkKid-8                  32437368                33.14 ns/op            0 B/op          0 allocs/op
BenchmarkKid-16                 33878541                35.40 ns/op            0 B/op          0 allocs/op
BenchmarkKid-32                 51138286                22.47 ns/op            0 B/op          0 allocs/op
BenchmarkXid                    44153774                27.32 ns/op            0 B/op          0 allocs/op
BenchmarkXid-2                  43354214                28.38 ns/op            0 B/op          0 allocs/op
BenchmarkXid-4                  36619646                28.60 ns/op            0 B/op          0 allocs/op
BenchmarkXid-8                  38258444                29.05 ns/op            0 B/op          0 allocs/op
BenchmarkXid-16                 48193314                32.18 ns/op            0 B/op          0 allocs/op
BenchmarkXid-32                 56045674                20.66 ns/op            0 B/op          0 allocs/op
goos: darwin
goarch: arm64
cpu: Apple M4 Max
BenchmarkKid                    38023658                31.19 ns/op            0 B/op          0 allocs/op
BenchmarkKid-2                  37417369                31.95 ns/op            0 B/op          0 allocs/op
BenchmarkKid-4                  39099517                30.96 ns/op            0 B/op          0 allocs/op
BenchmarkKid-8                  19487215                60.74 ns/op            0 B/op          0 allocs/op
BenchmarkKid-16                 15209077                79.36 ns/op            0 B/op          0 allocs/op
BenchmarkXid                    38485384                31.06 ns/op            0 B/op          0 allocs/op
BenchmarkXid-2                  36064644                30.28 ns/op            0 B/op          0 allocs/op
BenchmarkXid-4                  44846259                28.37 ns/op            0 B/op          0 allocs/op
BenchmarkXid-8                  26422333                46.76 ns/op            0 B/op          0 allocs/op
BenchmarkXid-16                 18502420                65.51 ns/op            0 B/op          0 allocs/op

For a broader comparison including ksuid, google/uuid (V4 and V7), ulid, and betterguid, run the suite in eval/bench on your own hardware.

Documentation

Overview

Package kid (K-sortable ID) provides a goroutine-safe generator of short (10 byte binary, 16 bytes when base32 encoded), url-safe, k-sortable unique IDs.

The 10-byte binary representation of an ID is composed of:

  • 6-byte value representing Unix time in milliseconds
  • 2-byte sequence, and,
  • 2-byte random value (ChaCha8, seeded by the Go runtime from OS entropy).

IDs encode (base32) as 16-byte URL-friendly strings. The encoding alphabet is in ascending ASCII order, so the encoded form preserves the sort order of the binary form: IDs are k-orderable in either representation.

kid.ID features:

  • Size: 10 bytes as binary, 16 bytes if stored/transported as an encoded string.
  • Timestamp + sequence is guaranteed to be unique and monotonically increasing within a process, even if the wall clock steps backwards.
  • Lock-free generation: New scales with cores instead of serializing on a mutex.
  • 2 bytes of trailing randomness from math/rand/v2's ChaCha8 generator.
  • K-orderable in both binary and base32 encoded representations.
  • URL-friendly custom encoding without the vowels a, i, o, and u.
  • Automatic (un)/marshalling for SQL and JSON.
  • The cmd/kid tool for ID generation and introspection.

Capacity: uniqueness is carried entirely by the timestamp+sequence pair, giving 4,096 IDs per millisecond (~4.1 million/second) sustained, per process. Bursts beyond that rate borrow sequence slots from future milliseconds: IDs remain unique and strictly k-sortable, but their embedded timestamps lead the wall clock until generation slows. Treat the embedded time as approximate metadata rather than an exact wall-clock instant.

Security note: an ID carries only 16 bits of randomness alongside values derived from the clock; IDs are predictable by design. Do not use kid IDs where unguessability matters, such as session tokens, API keys, or password reset codes.

Example usage:

func main() {
	id := kid.New()
	fmt.Printf("%s %03v\n", id, id[:])
	// Example output: 06bq7xhnr03mlz6r [001 149 115 246 021 192 007 073 252 216]

	id, err := kid.FromString("06bq7xhnr03mlz6r")
	if err != nil {
		// handle the error
	}
	fmt.Printf("%s %03v\n", id, id[:])
	// Output: 06bq7xhnr03mlz6r [001 149 115 246 021 192 007 073 252 216]
}

Acknowledgments:

While the ID payload differs greatly, the API and much of this package borrows heavily from https://github.com/rs/xid, a zero-configuration globally-unique ID generator. The timestamp+sequence encoding is derived from the google/uuid getV7Time() algorithm, with its mutex protection replaced by a lock-free atomic claim. Third-party copyright notices and license texts are reproduced in the NOTICES file.

Index

Examples

Constants

This section is empty.

Variables

View Source
var (

	// ErrInvalidID represents an error state, typically when decoding invalid input
	ErrInvalidID = errors.New("kid: invalid id")
)

Functions

func Sort

func Sort(ids []ID)

Sort sorts a slice of IDs in place, in ascending order.

Types

type ID

type ID [rawLen]byte

ID represents a unique identifier

func FromBytes

func FromBytes(b []byte) (ID, error)

FromBytes copies []bytes into an ID value. Only a length-check is performed.

func FromString

func FromString(str string) (ID, error)

FromString decodes a base32-encoded string to return an ID.

Example
id, err := FromString("03f6nlxczw0018fz")
if err != nil {
	panic(err)
}
fmt.Println(id.Timestamp(), id.Random())
Output:
946684799999 41439

func New

func New() (id ID)

New generates a new unique ID.

This function is goroutine-safe. IDs are composed of:

  • 6 bytes, timestamp, a Unix time in milliseconds
  • 2 bytes, sequence, a derived value ensuring uniqueness and order
  • 2 bytes, random value from math/rand/v2's ChaCha8 generator

New is lock-free and free of retry loops: the timestamp+sequence is claimed with at most one atomic compare-and-swap followed, if needed, by a wait-free atomic increment, and the random bytes are drawn from math/rand/v2's per-goroutine ChaCha8 state (seeded by the Go runtime from OS entropy), so ID generation scales with cores rather than serializing on a mutex or spinning under contention.

K-orderable: Each subsequent call to New() is guaranteed to produce an ID having a timestamp + sequence value greater than the previously generated ID.

Example

examples

id := New()
fmt.Printf(`ID:
    String()    %s
    Timestamp() %d
    Sequence()  %d
    Random()    %d
    Time()      %v
    Bytes()     %3v
`, id.String(), id.Timestamp(), id.Sequence(), id.Random(), id.Time().UTC(), id.Bytes())

func (ID) Bytes

func (id ID) Bytes() []byte

Bytes returns the binary representation of id, which is simply id[:].

func (ID) Compare

func (id ID) Compare(other ID) int

Compare returns an integer comparing two IDs with `bytes.Compare` semantics: 0 if the IDs are identical, -1 if id is less than other, and 1 if id is greater than other. All 10 bytes participate, so Compare is consistent with ==; because the timestamp and sequence occupy the leading bytes, IDs order by creation time first.

func (ID) Encode

func (id ID) Encode(dst []byte) []byte

Encode the id using base32 encoding, writing 16 bytes to dst and returning it. Encode panics if len(dst) < 16.

func (ID) IsNil

func (id ID) IsNil() bool

IsNil returns true if ID == nilID.

func (ID) IsZero

func (id ID) IsZero() bool

IsZero is an alias of IsNil.

func (ID) MarshalJSON

func (id ID) MarshalJSON() ([]byte, error)

MarshalJSON implements the json.Marshaler interface.

A json value will always be returned; as a nilID or any other binary ID will always encode, error will always be nil.

https://golang.org/pkg/encoding/json/#Marshaler

func (ID) MarshalText

func (id ID) MarshalText() ([]byte, error)

MarshalText implements `encoding.TextMarshaler`.

As any ID value will always encode, error is always nil. https://golang.org/pkg/encoding/#TextMarshaler

func (ID) Random

func (id ID) Random() int32

Random returns the two-byte random component of the ID.

func (*ID) Scan

func (id *ID) Scan(value any) error

Scan implements the sql.Scanner interface, accepting the 16-byte encoded form as a string or []byte, the 10-byte binary form as a []byte, or nil, which yields the nil ID. https://pkg.go.dev/database/sql#Scanner

func (ID) Sequence

func (id ID) Sequence() int32

Sequence returns the sequence component of id.

For IDs produced by New, the sequence is a 12-bit value (0-4095); if a burst of calls would overflow the sequence within a single millisecond, the overflow carries into the timestamp, preserving order (see getTS). The field occupies two bytes, so IDs from other sources may carry larger values.

func (ID) String

func (id ID) String() string

String implements `fmt.Stringer`, returning id as a base32 encoded string using the kid custom character set. https://pkg.go.dev/fmt#Stringer

func (ID) Time

func (id ID) Time() time.Time

Time returns the ID's timestamp as a Time value with millisecond resolution and location set to UTC

func (ID) Timestamp

func (id ID) Timestamp() int64

Timestamp returns the timestamp component of id as milliseconds since the Unix epoch. Go timestamps are at location UTC.

func (*ID) UnmarshalJSON

func (id *ID) UnmarshalJSON(b []byte) error

UnmarshalJSON implements the json.Unmarshaler interface, accepting only null or a quoted 16-character kid encoding. https://golang.org/pkg/encoding/json/#Unmarshaler

func (*ID) UnmarshalText

func (id *ID) UnmarshalText(text []byte) error

UnmarshalText implements `encoding.TextUnmarshaler`. text must be a 16-byte base32-encoded value over the kid alphabet; on error, id is set to the nil ID and ErrInvalidID is returned. https://pkg.go.dev/encoding#TextUnmarshaler

func (ID) Value

func (id ID) Value() (driver.Value, error)

Value implements package sql's driver.Valuer, returning the ID in its 16-byte encoded string form, or nil for the nil ID. https://pkg.go.dev/database/sql/driver#Valuer

Directories

Path Synopsis
cmd
kid command
A utility to generate or inspect kid.IDs.
A utility to generate or inspect kid.IDs.
eval
compare command
Package main produces for comparison purposes a markdown formatted table illustrating key differences between a number of unique ID packages.
Package main produces for comparison purposes a markdown formatted table illustrating key differences between a number of unique ID packages.
uniqcheck command
Command uniqcheck verifies, under concurrent load, the two guarantees made by kid.New:
Command uniqcheck verifies, under concurrent load, the two guarantees made by kid.New:

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL