uid: SafeShortID and HexShortID — Time-Ordered IDs Without the Rude Words
Random IDs have a problem nobody talks about at the interview: sometimes they spell words. Bad words. Words you do not want in a customer-facing URL, an invoice number, or a support ticket reference.
The dracory/uid package already had ShortID() — an 11-character, Crockford Base32, time-ordered ID. Now it has two siblings: HexShortID() and SafeShortID().
Try it — live ID generator
This is a faithful browser port of the Go generator: microsecond timestamp shifted left 4 bits, plus a rolling counter, encoded as fixed-width nibbles. It sorts chronologically, exactly like the real thing.
HexShortID: The Familiar One
HexShortID() gives you a fixed 14-character lowercase hexadecimal string:
id := uid.HexShortID() // "03fa81bc92e4d7"
Fourteen chars, always. No padding surprises, no variable length. It's the same 55-bit composite as ShortID() — microsecond timestamp shifted left 4 bits, plus a 4-bit rolling counter — just encoded in plain base 16 instead of base 32.
Use it when you want IDs that look like the hex strings everyone already recognises: database keys, log correlation IDs, anywhere a UUID would work but a UUID is overkill.
SafeShortID: The Polite One
SafeShortID() is the same 14-character format with one change: the alphabet has no vowels.
id := uid.SafeShortID() // "05fr88tc139r8t"
The alphabet is 0123456789bcdfgh — standard hex, but every vowel-adjacent letter is shifted: a becomes b, e becomes g, f becomes h. Sixteen characters, ascending byte order, zero vowels.
Alphabet map — see what gets filtered
Hover or tap a hex digit to see how SafeShortID re-encodes it. Letters that could form words land on safe consonants.
No vowels means no accidental English words. Your generated IDs can't spell "dead", "beef", "cafe", or anything ruder. It's not perfect — leet-speak like "b00b" is still reachable — but the surface area for embarrassment shrinks dramatically.
Use it anywhere an ID might be read aloud or shown to a customer.
The Engine Underneath
Both functions share the same lock-free generator as ShortID(). A package-level atomic.Uint64 packs (microsecond timestamp << 4) | counter into a single word, advanced with a CAS loop:
- Timestamp advances? Counter resets to zero.
- Same microsecond? Counter ticks up, up to 16 IDs per microsecond.
- Counter exhausted? The timestamp bumps forward one microsecond instead of sleeping. Callers never stall.
- Clock went backwards? The last timestamp is held and the counter keeps rolling.
Bit layout — inspect the last ID you generated
Because the alphabets are in ascending byte order, the output sorts chronologically — SafeShortID strings order correctly as plain strings, which makes them lovely as primary keys.
func SafeShortID() string {
return encodeHexFixed(nextShortIDValue(), safeShortAlphabet, 14)
}
The encoder is a fixed-width nibble walker — no modulo, no division, no allocations beyond the final string.
The Fine Print
Same caveats as ShortID(): uniqueness is per-process (two processes in the same microsecond can collide), the IDs are predictable, and they reveal their creation time. Great for sortable keys. Not for security tokens.
| Function | Length | Alphabet | Words? |
|---|---|---|---|
ShortID() | 11 | Crockford Base32 | Rarely |
HexShortID() | 14 | 0-9a-f | Possibly |
SafeShortID() | 14 | 0-9bcdfgh | Almost never |
Three generators, one engine. Pick the alphabet that fits where the ID will be seen.



