A Snowflake ID is a 64-bit integer assembled by bit-shifting three numbers into one word: a millisecond timestamp in the high bits, a machine identifier in the middle, and a per-millisecond sequence counter in the low bits. No coordination between servers, no round trip to a database, and the result sorts by creation time as a plain integer comparison. Twitter built it in 2010 to replace MySQL auto-increment once tweet storage moved to a sharded, non-relational backend. Discord adopted the same shape in 2015 for message and user IDs, with a different epoch and a slightly different split of the machine bits.
It's the same family as ULID and UUID v7 — timestamp first, so text and integer sort both equal creation order — but it predates both by years and fits in a native 64-bit integer column instead of 128 bits of text or binary.
How the 64 bits are packed
Both platforms use the same three-field idea; only the epoch and the exact width of the machine field differ.
| Bits (high to low) | Field | Discord | |
|---|---|---|---|
| 63 | Sign bit, always 0 | Reserved, unused | Not reserved — see below |
| 62–22 (41 bits) | Timestamp | ms since 2010-11-04 01:42:54.657 UTC | — |
| 63–22 (42 bits) | Timestamp | — | ms since 2015-01-01 00:00:00 UTC |
| 21–17 | Datacenter / worker | 5-bit datacenter ID | 5-bit internal worker ID |
| 16–12 | Worker / process | 5-bit worker ID | 5-bit internal process ID |
| 11–0 | Sequence | 12-bit per-ms counter, resets to 0 each new ms | 12-bit increment, resets to 0 each new ms |
That 41-vs-42 row isn't a typo — it's the one detail every secondary write-up on this topic glosses over or gets backwards.
Twitter's original Scala service kept bit 63 pinned to 0 so a Snowflake ID stays a positive signed 64-bit integer — its era cared about Java's
long, which is signed, and about the RDBMS and JSON clients of 2010 that choked on unsigned overflow. Discord's own reference docs describe the timestamp as bits 63 through 22, using the sign bit too — because Discord's API returns every Snowflake as a JSON string specifically to sidestep integer overflow, so nothing downstream ever treats the raw value as a signed native integer that could go negative. Same 64 bits, same three fields — one design reserves a bit for a constraint it no longer has to honor precisely because it made a different constraint (stringify everything) do that job instead.
The practical fallout: Twitter's 41-bit timestamp field runs out on 2080-07-10, worth knowing but not yours to fix. Discord's full 42 bits last until 2154-05-15. Neither is a today problem, but if you ever design your own variant, that's the tradeoff you're making when you decide whether to keep the sign bit.
Decoding a real one
Discord's developer reference publishes a worked example, and it's a good one to check your understanding against because you can verify it independently. Take the snowflake 175928847299117063 with Discord's epoch (1420070400000, the first millisecond of 2015):
| Step | Operation | Result |
|---|---|---|
| Sequence | id & 0xFFF (low 12 bits) | 7 |
| Machine field | (id >> 12) & 0x3FF (next 10 bits) | 32 → worker 1, process 0 |
| Timestamp delta | id >> 22 | 41,944,705,796 ms |
| Absolute time | delta + epoch | 1,462,015,105,796 ms since Unix epoch |
| UTC | — | 2016-04-30 11:18:25.796 UTC |
| Hex | — | 0x271065ac1020007 |
That matches Discord's own published breakdown for the same ID exactly — timestamp, worker, process and increment all line up. You can reproduce this yourself, or decode an arbitrary Snowflake against Twitter's, Discord's, or a plain Unix epoch, with the Snowflake ID generator — it does exactly this shift-and-mask arithmetic and shows the fields side by side.
Snowflake vs UUID v7 vs ULID: pick by where the sortability needs to live
All three exist to fix the same B-tree fragmentation problem that UUID v4 has — random keys land at random leaves. They differ in what they cost to get it.
| Snowflake | UUID v7 | ULID | |
|---|---|---|---|
| Size | 64 bits | 128 bits | 128 bits |
| Fits a native integer column | Yes (bigint) | No (uuid/binary) | No (text/binary) |
| Needs a coordinated machine ID | Yes | No | No |
| Timestamp resolution | 1 ms | 1 ms | 1 ms |
| Standardized | No (platform-specific convention) | RFC 9562 | Community spec |
| Text form | Just digits, no fixed length | 36 chars, hyphenated | 26 chars, Crockford base32 |
The row that actually decides it: Snowflake needs a machine ID, the other two don't. UUID v7 and ULID pack enough randomness into the low bits that two generators anywhere on the planet can mint IDs with no coordination and negligible collision risk. Snowflake's low bits are a small sequence counter, not entropy — collision-freedom comes entirely from every worker having a unique, correctly-assigned machine ID. That's a real operational commitment (a config value, a registry, a Kubernetes pod ordinal) that v7 and ULID simply don't ask you to make. In exchange, Snowflake gives you a value that fits in eight bytes and a bigint column instead of sixteen bytes and a type your database didn't ship a decade of tooling around.
If you're generating IDs from many uncoordinated processes and don't want to own machine-ID assignment, take UUID v7 or ULID. If you're already running a fleet with stable worker identities — the exact shape of Twitter's and Discord's own infrastructure — Snowflake's smaller footprint is a real, quantifiable win at scale.
Two mistakes that only show up in production
The clock rolled backward
Every field above assumes the timestamp only moves forward. NTP doesn't guarantee that — a step correction can move a server's clock backward by milliseconds or seconds. If your generator naively reads the clock and shifts it into the high bits, a backward step produces a timestamp smaller than one it already emitted, and now two different IDs can compare as if the older one were created later — or, worse, collide outright if the sequence counter also happens to line up.
Every serious implementation (Twitter's original service, and successors like Sonyflake) handles this the same way: detect now < lastTimestamp and refuse to generate, blocking or erroring until the clock catches back up rather than silently emitting a value that violates the ordering guarantee the whole format exists to provide. If you're evaluating a Snowflake library, this is the one line of its source worth reading before you trust it in production — plenty of toy implementations skip the check entirely.
JavaScript will silently corrupt it
A Snowflake ID is a 64-bit integer. JavaScript's Number only represents integers exactly up to Number.MAX_SAFE_INTEGER — 9007199254740991, a 53-bit limit. Every real Snowflake ID is well past that: 175928847299117063 from the example above is 18 digits. Run it through JSON.parse as a bare number and you get this back, with no error thrown:
JSON.parse("175928847299117063"); // 175928847299117060 — silently wrongThree digits gone, no exception, no warning — the kind of bug that surfaces as "why do two IDs that should be equal not match" three services downstream of where it actually happened. This is exactly why Discord's HTTP API returns every Snowflake as a JSON string, not a number: strings don't get rounded. If you're consuming Snowflake IDs from any API in a JavaScript or TypeScript client, keep them as strings (or BigInt) the entire way through your code — the moment one touches Number, arithmetic or not, the low digits are no longer guaranteed correct.
FAQ
Can two workers generate the same Snowflake ID?
Yes, if they're assigned the same machine ID and mint an ID in the same millisecond with the same sequence value — which is exactly why machine-ID assignment has to be authoritative (a config value nobody duplicates by copy-pasting a deployment, a coordinator like ZooKeeper in Twitter's original design, or a value derived from something guaranteed unique like a pod ordinal). This is the tradeoff called out in the comparison above: Snowflake's uniqueness is an operational guarantee you maintain, not a probabilistic one baked into the bits.
Is the sequence counter shared across a whole service, or per-request?
Per generator process — that's the point of the machine ID field existing alongside it. Each worker owns its own sequence counter and its own slice of the machine-ID space, so two workers can each hand out sequence 0 through 4095 in the same millisecond without colliding, because their machine IDs differ.
Do I need Twitter's or Discord's exact epoch, or can I pick my own?
Pick your own if you're building a new system — the epoch only sets where the 41- or 42-bit timestamp clock starts counting from, which determines how far into the future the field can represent before it overflows. A more recent epoch than Unix time (either platform's, or today's date) buys you the same number of years further out. What has to match Twitter's or Discord's exact epoch is decoding their IDs — get the epoch wrong and every other field in the breakdown above comes out wrong too, silently.
The IDs look arbitrary until you know the shift-and-mask arithmetic behind them, and then every one you see is a timestamp and a worker number wearing a disguise. If you're deciding whether to build one into your own schema, decode a few real examples first — the Snowflake ID generator will take a Twitter, Discord, or Unix-epoch value apart for you the same way this article just did by hand.
Cover photo by sergeispas on Pexels.
