A Snowflake ID looks like an ordinary large integer, but it is really a packed record: most of its bits are a millisecond timestamp, and the rest identify the machine that created it and a per-millisecond counter. Once you know the layout, you can decode a Snowflake ID timestamp with two bit operations and an addition. This guide explains the 64-bit structure, the epochs used by Twitter/X and Discord, and how to do the decoding yourself in JavaScript, Python, PHP and SQL. If you only need the answer for a specific ID, paste it into the Snowflake ID decoder.
The 64-bit layout of a Snowflake ID
Twitter introduced the scheme so that many servers could create unique, roughly time-ordered IDs without coordinating with each other. The classic layout, read from the most significant bit down, is:
| Bits | Field | Purpose |
|---|---|---|
| 1 | Sign bit | Always 0, so the ID stays positive in a signed 64-bit integer |
| 41 | Timestamp | Milliseconds elapsed since a custom epoch |
| 10 | Machine ID | Twitter splits it into 5 bits of datacenter and 5 bits of worker |
| 12 | Sequence | Counter for IDs created by the same worker in the same millisecond |
Because the timestamp occupies the high bits, shifting the ID right by 22 places (10 + 12) removes everything except the time. The remaining fields are recovered by shifting and masking with 0x1F (5 bits) or 0xFFF (12 bits).
The 12-bit sequence allows 4,096 IDs per worker per millisecond. When a worker runs out, a typical generator waits for the next millisecond before issuing more.
Epochs: why the same number means different dates
The timestamp is not counted from the Unix epoch (1970) but from an epoch chosen by each system, which buys decades of extra range. The two most common public layouts are:
| Platform | Epoch (Unix ms) | Epoch (UTC) | Low 22 bits |
|---|---|---|---|
| Twitter / X | 1288834974657 | 2010-11-04 01:42:54.657 | 5 datacenter, 5 worker, 12 sequence |
| Discord | 1420070400000 | 2015-01-01 00:00:00 | 5 internal worker, 5 internal process, 12 increment |
Discord documents its format in the Discord developer reference. Decoding an ID with the wrong epoch still produces a plausible-looking date, just years off, so always confirm where an ID came from. The decoder offers presets for both platforms, several others, and a custom epoch for your own services.
How to decode a Snowflake ID timestamp by hand
Take 175928847299117063, the example ID from Discord's documentation. Decoding it takes four steps:
- Shift right 22 bits:
175928847299117063 >> 22 = 41944705796. This is milliseconds since the Discord epoch. - Add the epoch:
41944705796 + 1420070400000 = 1462015105796, a normal Unix timestamp in milliseconds. - Convert to a date:
1462015105796ms is2016-04-30T11:18:25.796Z. - Extract the other fields: worker
(id >> 17) & 0x1F = 1, process(id >> 12) & 0x1F = 0, incrementid & 0xFFF = 7.
The same ID in the Snowflake decoder with the Discord preset produces this output, including the full binary so you can see where each field starts:
# Layout: Discord, epoch 2015-01-01T00:00:00.000Z
175928847299117063
Timestamp: 2016-04-30T11:18:25.796Z (10.4 years ago)
Unix ms: 1462015105796
Internal worker: 1
Internal process: 0
Increment: 7
Binary: 00000010 01110001 00000110 01011010 11000001 00000010 00000000 00000111
The relative age in brackets depends on the day you run it. To view the Unix milliseconds in another time zone, use the Unix timestamp converter; to experiment with the shifts and masks, try the bitwise calculator or view the ID in base 2 with the decimal to binary converter.
Decoding Snowflakes in code
JavaScript (BigInt)
const DISCORD_EPOCH = 1420070400000n;
function decodeSnowflake(idString, epoch = DISCORD_EPOCH) {
const id = BigInt(idString);
const ms = (id >> 22n) + epoch;
return {
date: new Date(Number(ms)).toISOString(),
worker: Number((id >> 17n) & 0x1Fn),
process: Number((id >> 12n) & 0x1Fn),
sequence: Number(id & 0xFFFn),
};
}
decodeSnowflake('175928847299117063');
// { date: '2016-04-30T11:18:25.796Z', worker: 1, process: 0, sequence: 7 }
Python
from datetime import datetime, timezone
TWITTER_EPOCH = 1288834974657
def decode(snowflake, epoch=TWITTER_EPOCH):
sid = int(snowflake)
ms = (sid >> 22) + epoch
return {
'time': datetime.fromtimestamp(ms / 1000, tz=timezone.utc).isoformat(timespec='milliseconds'),
'datacenter': (sid >> 17) & 0x1F,
'worker': (sid >> 12) & 0x1F,
'sequence': sid & 0xFFF,
}
decode('1541815603606036480')
# {'time': '2022-06-28T16:07:40.105+00:00', 'datacenter': 11, 'worker': 26, 'sequence': 0}
Python integers have arbitrary precision, so no special type is needed.
PHP and PostgreSQL
On 64-bit PHP builds, integers are signed 64-bit, which holds any Snowflake whose sign bit is 0:
$id = (int) '175928847299117063';
$ms = ($id >> 22) + 1420070400000;
$dt = DateTimeImmutable::createFromFormat('U.v', sprintf('%d.%03d', intdiv($ms, 1000), $ms % 1000));
echo $dt->format('Y-m-d\TH:i:s.v\Z'); // 2016-04-30T11:18:25.796Z
In PostgreSQL, a bigint column can be decoded directly in a query:
SELECT to_timestamp(((175928847299117063::bigint >> 22) + 1420070400000) / 1000.0) AT TIME ZONE 'UTC';
-- 2016-04-30 11:18:25.796
Why JavaScript numbers corrupt Snowflake IDs
A JavaScript Number is a 64-bit float with a 53-bit mantissa, so integers are exact only up to Number.MAX_SAFE_INTEGER (9007199254740991, which is 2^53 − 1). Snowflakes created more than about 25 days after their epoch are larger than that, and JSON.parse silently rounds them:
JSON.parse('{"id": 175928847299117063}').id
// 175928847299117060 (the last digits changed)
The rounded value points to a different ID, which leads to lookups that fail for no obvious reason. The fixes are simple:
- Transport IDs as strings. Twitter's API added
id_strfor this reason, and Discord's API returns snowflakes as strings. - Convert with
BigInt(idString)only when you need arithmetic, as in the example above. - Store IDs in a
BIGINTdatabase column, never in a floating-point or double column.
Custom epochs: decoding IDs from your own service
Many in-house ID generators copy the Twitter layout but pick their own epoch, often the date the project launched. To decode such IDs, choose the Custom epoch option and enter the epoch in Unix milliseconds. With an epoch of 1704067200000 (2024-01-01 UTC), the tool gives:
# Layout: Twitter / X (custom epoch), epoch 2024-01-01T00:00:00.000Z
158868548466192689
Timestamp: 2025-03-14T09:26:53.589Z (1.6 years ago)
Unix ms: 1741944413589
Datacenter: 7
Worker: 19
Sequence: 305
Binary: 00000010 00110100 01101010 00011010 01100101 01001111 00110001 00110001
The custom option assumes the classic 41/5/5/12 split. If your generator allocates the low 22 bits differently, the timestamp is still correct, but you will need to re-slice the lower fields yourself.
Ordering, the 69-year limit and clock skew
Snowflakes sort roughly by creation time, which makes them good primary keys for feeds and message logs. A few properties are worth knowing:
- Range queries by time: the smallest possible ID for a moment is
(ms − epoch) << 22. For Discord at 2016-05-01 00:00 UTC that is176120502681600000, soid >= 176120502681600000selects everything from that day onward. - Ordering is only approximate across machines: two IDs from the same millisecond on different workers sort by their worker bits, not by which was actually created first.
- The range is finite: 41 bits of milliseconds cover about 69.7 years. Twitter's IDs run out in July 2080 and Discord's in September 2084, after which the timestamp would spill into the sign bit. The decoder reports a number with that bit set (anything above 9223372036854775807) as not a Snowflake.
- Clock skew matters: if a server's clock jumps backwards, it could reissue a timestamp it has already used. Well-behaved generators refuse to issue IDs or wait until the clock catches up. A decoded date is only as accurate as the generating machine's clock.
Key takeaways
To decode a Snowflake, shift right by 22 bits, add the platform's epoch and read the result as Unix milliseconds. Keep IDs as strings or BigInt in JavaScript, and double-check the epoch before trusting a date. For identifiers that are 128-bit and text-based instead of 64-bit integers, see the companion guide ULID vs UUID and the ULID decoder.