Twitter (X) Snowflake IDs: the 64-bit Layout and How to Decode One

This page explains the ID format itself: where the 64 bits go, why the epoch is 1288834974657, how to decode an ID in Python, JavaScript and SQL, and where the formula silently fails. If you only want the answer for one post, paste it into our free tweet ID to date tool at /tools/twitter-timestamp; if you need to turn a handle into its numeric user ID, use the ID finder at /tools/twitter-id-finder. Both are linked at the end. Our guide to the X-API-Key header and the Python guide cover authentication and requests; this page is only about what is inside the number.
Most explainers stop at the bit diagram and a one-line formula. We wanted to know whether the formula still holds for real data, so we pulled IDs and createdAt values straight from the API and compared them: 1,000 current posts from 10 public accounts, 170 posts from the weeks around the November 2010 switch, and the creation records of 1,072 accounts going back to 2006. The short version: for posts it is exact to the second, for user accounts it is not, and for anything older than November 2010 it is not a Snowflake at all.
Snowflake also became a pattern well beyond Twitter. Discord, Mastodon and many in-house systems use the same idea with a different epoch or bit split, and libraries such as @sapphire/snowflake on npm ship both a Twitter and a Discord decoder. The last section shows how the variants differ so you apply the right constant.
What a Twitter Snowflake ID is
Snowflake is the service Twitter wrote in 2010 to hand out unique IDs without a central counter. The original README in github.com/twitter-archive/snowflake (the snowflake-2010 tag) lists the requirements: at least 10,000 IDs per second per process, a response in about 2 ms, no coordination between machines, IDs that are roughly time ordered (k-sorted within about a second) and directly sortable, and a size under 64 bits. Generating IDs on many machines at once, with each machine stamping its own clock time and its own machine number, meets all of them.
The repository is no longer maintained. Its current README says Twitter retired the initial release, will not accept pull requests or answer issues for it, and that the source stays reachable from the snowflake-2010 tag; GitHub shows the repository as archived on September 18, 2021. The ID format did not retire with it. X's developer documentation (docs.x.com/fundamentals/x-ids) still says X IDs are 64-bit unsigned integers generated by a system called Snowflake, each encoding a timestamp, a worker number and a sequence number, and that every object in the API, posts, users, lists, DMs and Spaces, has one.
Why it matters to you. Because the creation time sits inside the ID, you can tell when a post was written from its URL alone, sort posts without fetching them, and turn a date into an ID boundary for search. Because the ID is wider than 53 bits, you can also corrupt it by accident in any language that stores numbers as 64-bit floats. Both sides are covered below.
The 64-bit layout, bit by bit
The archived IdWorker.scala builds every ID with one expression: ((timestamp - twepoch) << 22) | (datacenterId << 17) | (workerId << 12) | sequence, with twepoch = 1288834974657L, datacenterIdBits = 5, workerIdBits = 5 and sequenceBits = 12. Read from the most significant bit down, that gives:
The epoch. 1288834974657 ms is Thursday, November 4, 2010, 01:42:54.657 UTC. It is not a round number because Twitter took the moment it set the constant, not a calendar boundary. The README says 41 bits of milliseconds with a custom epoch gives about 69 years, so the timestamp field runs out around 2080.
The machine bits. The README describes 10 bits of configured machine ID, up to 1,024 machines; the code splits them into 5 bits of datacenter and 5 bits of worker. X has never documented what its current datacenter and worker numbers mean, so treat them as opaque. In our 1,000 recent post IDs the datacenter field took only two values (11 and 13), and the worker field took 30 of its 32 possible values.
The sequence. Within one millisecond on one worker, the 12-bit counter numbers the IDs 0 to 4,095, then the generator waits for the next millisecond. That is why two IDs from the same worker in the same millisecond still differ and still sort in issue order.
Why IDs sort by time. The timestamp occupies the highest bits, so a larger ID almost always means a later post. Across machines the order is only as good as their clocks (the README promised about a second), which is why X calls them roughly time ordered, not strictly ordered.
Decode a tweet ID to a timestamp (Python, JavaScript)
The tweet ID is the number at the end of a post URL: in x.com/<user>/status/1234567890123456789 it is everything after status/. Decoding is two integer operations and needs no API call.
Python. Python integers are arbitrary precision, so (int(tweet_id) >> 22) + 1288834974657 is exact; divide by 1,000 and pass it to datetime.fromtimestamp(..., tz=timezone.utc). The complete script at the end of this page does that for a whole timeline and checks each result against createdAt.
JavaScript. Do not use Number. A 61-bit ID does not fit a double: when we pushed our 1,000 real IDs through a float64, 998 of them changed, by up to 128. The timestamp bits happened to survive in all 1,000 cases, but the ID itself is wrong, so any lookup with it fails. Use BigInt as in the snippet below, and when you read API JSON keep the ID as a string (twitterapi.io returns id as a string; X's v1.1 payloads carry id_str for the same reason).
Library option. @sapphire/snowflake (MIT, version 3.5.5 on npm, BigInt based) exports TwitterSnowflake and DiscordSnowflake with a deconstruct() method that returns the timestamp, worker, process and increment fields. It is a convenient choice when you already handle Discord IDs in the same codebase.
A worked example. Take post 298781148975104 by @ev, the first Snowflake-era post in our 2010 sample. 298781148975104 >> 22 is 71,234,977, the milliseconds after the epoch (about 19.79 hours). Adding 1288834974657 gives 1288906209634, which is 2010-11-04 21:30:09.634 UTC. The API reports createdAt as Thu Nov 04 21:30:09 +0000 2010: the same second, with the 634 ms that the string format drops. The low bits read datacenter 0, worker 1, sequence 0.
// Decode a Twitter (X) Snowflake ID in Node.js or the browser. Keep the ID a string.
const TWEPOCH = 1288834974657n;
function decodeSnowflake(idStr) {
const id = BigInt(idStr);
const ms = (id >> 22n) + TWEPOCH;
return {
createdAt: new Date(Number(ms)).toISOString(), // ms fits safely in a Number
datacenter: Number((id >> 17n) & 0x1Fn),
worker: Number((id >> 12n) & 0x1Fn),
sequence: Number(id & 0xFFFn),
};
}
function idFromUrl(url) {
const m = url.match(/status(?:es)?\/(\d+)/);
return m ? m[1] : null;
}
console.log(decodeSnowflake("298781148975104"));
// { createdAt: '2010-11-04T21:30:09.634Z', datacenter: 0, worker: 1, sequence: 0 }
// Wrong: Number("2112847277282721888") === 2112847277282721800, a different post.
We decoded 1,000 real tweet IDs: how exact is it?
We pulled the 100 most recent posts of each of 10 public accounts (@NASA, @github, @nvidia, @Xbox, @SpaceX, @NBA, @BBCWorld, @elonmusk, @jack and @Microsoft) through /twitter/user/last_tweets, decoded every id with Python integers and subtracted the createdAt timestamp the API returns for the same post.
Reading the offsets. createdAt comes in the form Thu Nov 04 21:30:09 +0000 2010, with whole seconds only, so a correct decode must land between 0 and 999 ms after it. Every one did, and the offsets are spread evenly across that range (chart below), which is what you expect when the API simply truncates the milliseconds. There is no sign of a systematic shift between the clock that stamped the ID and the clock that produced createdAt.
What the sequence field shows. The 2010 code resets the sequence to 0 at each new millisecond, so at low load most IDs would end in a small number. Ours did not: only 0.1% had a sequence of 0 and the median was 258. Either X's current generator assigns the low bits differently from the 2010 release or load is high enough that the counter rarely starts fresh; X does not document which. For decoding it does not matter, because the timestamp is in the top bits.
Practical takeaway. For posts, the ID is a better timestamp than createdAt: it carries milliseconds, and you can read it from a URL without any request. Our free /tools/twitter-timestamp tool runs this exact decode for a batch of URLs or IDs and exports a CSV.

User IDs: Snowflakes only since 2016, and they run early
User IDs are not all Snowflakes. Twitter kept handing out sequential user IDs long after posts switched, so any account old enough has an ID that is just a counter. We collected the creation records of 1,072 accounts that appeared as authors of Top search results, as followers and followings of @NASA, and in user search results, and checked each ID against its createdAt.
The switch. The newest legacy user ID in our sample belongs to an account created in February 2016; it is 10 digits (33 bits). The oldest Snowflake user ID belongs to an account created in April 2016. So for user IDs the change happened in the first half of 2016. A legacy user ID shifted right by 22 gives a number of milliseconds so small that the decode lands within about a second of the 2010 epoch: obviously wrong for an account from 2009, and plausible-looking for one from late 2010.
The surprise. Snowflake user IDs from 2016 to 2019 behave like post IDs: 85.2% decode to the exact createdAt second. From 2020 on, almost none do. For the 659 accounts created in 2021 or later, the decoded time is a median of 47 seconds before createdAt, the 90th percentile is 174 seconds early, and not a single one decodes later than createdAt. That pattern is consistent with the ID being reserved at the start of sign-up and createdAt being written when sign-up completes, but X does not document its sign-up flow, so take that as our reading, not a fact.
Practical takeaway. For account age, read createdAt from /twitter/user/info (18 credits per profile, $0.18 per 1,000) instead of decoding the user ID. A decoded user ID is fine for a rough year, or for ordering accounts, but it can be off by minutes for current accounts and by years for legacy ones. Our /tools/twitter-account-age-checker reads the profile's creation date rather than decoding the ID, and /tools/twitter-id-finder converts a handle to its numeric ID if you need the ID itself.

Before November 2010: when the formula gives nonsense
Posts written before the switch carry the old sequential IDs, and the formula does not reject them, it quietly returns a time near the epoch. To find the exact boundary we searched the posts of @jack, @biz and @ev around the switch and pulled post 20, the first post on the service, by ID: 170 posts in total, 52 with legacy IDs and 118 with Snowflakes.
The boundary. In our sample the last legacy ID appeared at 20:20:45 UTC on November 4, 2010 and the first Snowflake 1.16 hours later, about 20 hours after the epoch constant itself. All 118 Snowflake-era posts in the sample decoded to their exact createdAt second. The ID jumped from 11 digits to 15 digits, and because the first Snowflakes were already far larger than the last legacy ID, sorting by ID still matched sorting by time across the switch for every post in our sample.
A safe guard. In our data every legacy post ID was 11 digits or fewer (the last one about 29.7 billion): treat such an ID as legacy and fetch createdAt instead of decoding it. In code, a threshold of 2**40 (about 1.1 trillion) works for both posts and users: every legacy ID in our data, post or user, was far below it (the largest legacy user ID had 33 bits) and every Snowflake far above it (the smallest had 49 bits).
If you work with old archives, the /twitter/tweets endpoint takes a comma-separated list in tweet_ids and returns createdAt for each post (15 credits per post returned), which is the reliable way to date pre-2010 posts.
Run it backward: timestamps to IDs, search windows and SQL
Because the layout is public, you can build an ID from a time: (ms - 1288834974657) << 22 is the smallest possible ID for that millisecond. That is useful in two places.
Exact search windows. The search endpoint accepts X's since_id: and max_id: operators. We built both bounds from midnight UTC timestamps one week apart and sent from:NASA since_id:<start> max_id:<end> to /twitter/tweet/advanced_search: 20 of 20 posts returned fell inside the window. since: and until: take calendar dates; ID bounds work to the millisecond, and they are a natural checkpoint: store the largest ID you have seen and pass it as since_id: on the next run.
Cursorless backfills. If a long backfill stops, you can restart it from the last ID you stored without keeping a cursor, because the ID encodes where you were in time.
SQL. Every major database can decode IDs stored as 64-bit integers (Snowflakes fit in a signed BIGINT because the top bit is 0). The snippet shows PostgreSQL, MySQL and BigQuery; store the column as BIGINT, or decode from text with a cast.
Cost of pulling the reference data. Decoding is free. If you also want createdAt from the API to check or enrich IDs, twitterapi.io bills tweet endpoints at 15 credits per post returned, $0.15 per 1,000 posts, and profiles at 18 credits each, $0.18 per 1,000. The official X API pay-per-usage rate card (docs.x.com/x-api/getting-started/pricing) lists $0.005 per post read and $0.010 per user read, which is $5 per 1,000 posts, about 33 times more per 1,000 posts. No X account is needed for reads; see /pricing for plans.
-- PostgreSQL: id BIGINT
SELECT id,
to_timestamp(((id >> 22) + 1288834974657) / 1000.0) AT TIME ZONE 'UTC' AS created_utc
FROM tweets;
-- MySQL: id BIGINT UNSIGNED
SELECT id,
FROM_UNIXTIME(((id >> 22) + 1288834974657) / 1000) AS created_utc
FROM tweets;
-- BigQuery: id INT64 (or SAFE_CAST(id_str AS INT64))
SELECT id,
TIMESTAMP_MILLIS((id >> 22) + 1288834974657) AS created_utc
FROM dataset.tweets;
-- Timestamp to the smallest ID for that moment (PostgreSQL)
SELECT ((EXTRACT(EPOCH FROM now() - INTERVAL '7 days') * 1000)::BIGINT
- 1288834974657) << 22 AS since_id_7_days_ago;
Discord, Mastodon and other Snowflake variants
Snowflake is now a family. The idea is always the same, a millisecond timestamp in the high bits and machine plus counter bits below, but the epoch and the field split change, and using the wrong epoch shifts every date by years.
Discord. Discord's developer reference documents a timestamp in bits 63 to 22 counted from the first second of 2015, then a 5-bit internal worker ID, a 5-bit internal process ID and a 12-bit increment. The shift is the same as Twitter's; only the epoch differs. Discord's table counts the timestamp as 42 bits because it includes the top bit in the field.
Mastodon. Mastodon's API guidelines say IDs are generated with a variant of Snowflake but must always be treated as opaque strings, never cast to integers, and that other servers may not use numbers at all. Sort them by length, then lexically, if you need to.
Your own system. If you generate IDs yourself, the 2010 Twitter source is still a clear reference for the pattern: pick an epoch close to your launch to extend the 69-year range, assign worker IDs from configuration, and wait for the next millisecond when the 12-bit counter overflows. Copy the pattern, not Twitter's epoch.
How we collected this
Question. Does (id >> 22) + 1288834974657 reproduce the createdAt the API returns, for posts and for user accounts, and where does it stop applying?
Posts. The 100 most recent posts of each of 10 public organization and public-figure accounts (@NASA, @github, @nvidia, @Xbox, @SpaceX, @NBA, @BBCWorld, @elonmusk, @jack, @Microsoft) via /twitter/user/last_tweets, 1,000 posts, collected in 2026.
The 2010 switch. 170 posts by @jack, @biz and @ev from the weeks around November 2010 via /twitter/tweet/advanced_search with since:/until: dates, plus post 20 via /twitter/tweets.
Users. 1,072 distinct accounts: authors of Top results for neutral topic searches, recent followers and followings of @NASA, and user search results. These are mostly private individuals, so we keep only aggregates (creation year, ID size, offset bucket); no handle, ID or text is published.
Since-ID check. One /twitter/tweet/advanced_search call for @NASA with since_id:/max_id: bounds built from timestamps a week apart.
Size and cost. 91 requests in total; about 1,550 posts returned, about $0.23 at $0.15 per 1,000, plus a few cents of follower and user-search reads.
What the sample cannot show. The user sample is skewed toward new accounts (a large share are recent @NASA followers) and has few accounts from 2015 to 2019, so the exact month of the user-ID switch and the year the early offset began are bounded, not pinned. We cannot see X's internal ID service, so the meaning of the datacenter and worker values, and the reason user IDs run early, are interpretations.
Reproduce it. The analysis and chart scripts, the protocol and the results are in the study folder data/studies/twitter-snowflake/ of this site's repository; raw pages are committed only for the public accounts.
"""Decode Twitter (X) Snowflake IDs and check them against createdAt.
pip install requests
export TWITTERAPI_IO_KEY=... # from https://twitterapi.io/dashboard
"""
import os
from datetime import datetime, timezone
import requests
BASE = "https://api.twitterapi.io"
HEADERS = {"X-API-Key": os.environ["TWITTERAPI_IO_KEY"]}
TWEPOCH = 1288834974657 # ms, 2010-11-04 01:42:54.657 UTC
def decode(snowflake_id):
i = int(snowflake_id) # Python ints are exact; never float()
if i < 2 ** 40:
raise ValueError("legacy pre-Snowflake ID, fetch createdAt instead")
ms = (i >> 22) + TWEPOCH
return {
"utc": datetime.fromtimestamp(ms / 1000, tz=timezone.utc),
"ms": ms,
"datacenter": (i >> 17) & 0x1F,
"worker": (i >> 12) & 0x1F,
"sequence": i & 0xFFF,
}
def id_for_time(dt):
"""Smallest possible Snowflake for a moment: use as since_id / max_id."""
return (int(dt.timestamp() * 1000) - TWEPOCH) << 22
def last_tweets(user, pages=2):
cursor = ""
for _ in range(pages):
r = requests.get(f"{BASE}/twitter/user/last_tweets",
headers=HEADERS,
params={"userName": user, "cursor": cursor},
timeout=30)
r.raise_for_status()
body = r.json()
for t in body.get("data", {}).get("tweets", []):
yield t
if not body.get("has_next_page"):
break
cursor = body.get("next_cursor", "")
def check(user):
rows = []
for t in last_tweets(user):
d = decode(t["id"])
api = datetime.strptime(t["createdAt"], "%a %b %d %H:%M:%S %z %Y")
offset_ms = d["ms"] - int(api.timestamp()) * 1000
rows.append(offset_ms)
print(t["id"], d["utc"].isoformat(timespec="milliseconds"),
t["createdAt"], f"{offset_ms} ms")
same = sum(0 <= o < 1000 for o in rows)
print(f"{same}/{len(rows)} IDs decode to the createdAt second")
def search_window(user, start, end):
"""Posts between two datetimes, bounded by Snowflake IDs."""
q = f"from:{user} since_id:{id_for_time(start)} max_id:{id_for_time(end)}"
r = requests.get(f"{BASE}/twitter/tweet/advanced_search",
headers=HEADERS,
params={"query": q, "queryType": "Latest"},
timeout=30)
r.raise_for_status()
body = r.json()
return body.get("tweets", []), body.get("has_next_page"), body.get("next_cursor")
if __name__ == "__main__":
check("NASA") # 2 pages = up to 40 posts = about $0.006
week_end = datetime.now(timezone.utc)
week_start = datetime.fromtimestamp(week_end.timestamp() - 7 * 86400, tz=timezone.utc)
tweets, more, _ = search_window("NASA", week_start, week_end)
print(len(tweets), "posts in the last 7 days (first page), more:", more)
Questions readers ask
What is a Twitter Snowflake ID?
It is the 64-bit unique ID X (Twitter) assigns to posts, users, lists, DMs and Spaces. It packs a 41-bit millisecond timestamp since 1288834974657 (November 4, 2010), 5 bits of datacenter, 5 bits of worker and a 12-bit sequence, so IDs are unique without a central counter and sort roughly by creation time.
How do I convert a tweet ID to a date?
Shift the ID right by 22 bits and add 1288834974657: the result is the creation time in Unix milliseconds (UTC). In Python: datetime.fromtimestamp(((int(i) >> 22) + 1288834974657) / 1000, tz=timezone.utc). Or paste the ID or URL into our free /tools/twitter-timestamp tool. In our test on 1,000 real IDs the result always matched the API createdAt to the second.
What is the Twitter epoch 1288834974657?
It is the twepoch constant in Twitter's archived snowflake source (IdWorker.scala), in milliseconds since the Unix epoch. It corresponds to November 4, 2010, 01:42:54.657 UTC. Discord uses a different epoch, 1420070400000 (January 1, 2015), so never mix the two.
Can I get an account's creation date from its user ID?
Only roughly. Accounts created before 2016 have legacy sequential IDs that do not decode at all, and in our sample accounts created from 2021 on decode a median of 47 seconds before their real createdAt. Read createdAt from the profile instead, for example with /twitter/user/info or our account age checker.
Why does my tweet ID change in JavaScript?
Snowflake IDs need 61 bits and a JavaScript Number holds integers exactly only up to 53 bits, so parsing an ID as a number rounds it. In our test 998 of 1,000 IDs changed. Keep IDs as strings and use BigInt for bit operations; X's v1.1 API added id_str for this reason.
Do older tweets have Snowflake IDs?
No. Posts before November 4, 2010 have sequential IDs of up to 11 digits, such as 20 for the first post in 2006. Run through the formula they decode to a moment near the 2010 epoch. Treat any ID under 2 to the power of 40 as legacy and fetch createdAt for it.
Are Twitter IDs strictly in time order?
Roughly. The timestamp sits in the highest bits, so a larger ID nearly always means a later post, and in our sample ID order matched time order for every neighbor pair. Across machines the order depends on clock sync, which the original design promised to about one second, so do not rely on it for sub-second ordering between different posts.
Is Twitter's snowflake code still maintained?
No. The twitter-archive/snowflake repository says the initial release is retired, accepts no pull requests or issues, and keeps the source under the snowflake-2010 tag; GitHub shows it archived since September 18, 2021. The ID format itself is still documented by X and still used in every API response.
Continue
- Twitter snowflake-2010 source and README: requirements and 41/10/12-bit ID composition
- twitter-archive/snowflake README: initial release retired, source kept under the snowflake-2010 tag (IdWorker.scala holds twepoch 1288834974657)
- X developer docs: X IDs are 64-bit Snowflake IDs; use id_str, JavaScript 53-bit limit
- Discord developer reference: Snowflake format and the 1420070400000 epoch
- Mastodon API guidelines: IDs are a Snowflake variant, treat as opaque strings
- twitterapi.io API documentation: last_tweets, advanced_search, tweets by ID, user info
- X (Twitter) API overview: endpoints, pricing and access
- Free tweet ID to exact date tool (batch, CSV export)
- Twitter ID finder: username to numeric ID and back
- Check when an X account was created
- Advanced search API guide: operators, pagination and windows
- How the X-API-Key header authenticates requests
- Twitter (X) API in Python: complete guide
- twitterapi.io pricing: pay only for the data returned
- 01X Radar Explained: Twitter (X) Keyword Monitoring, Tiers, Limits and an API Alternative
- 02Twitter (X) Stalker Tools: What They Can See, What They Can't, and Where the Line Is
- 03Snaplytics Twitter (X) Viewer: What It Is, Whether It Works, and Safer Options
- 04Tweeload: What the Twitter (X) Video Downloader Does, and Whether It Is Safe
- 05Twillot Twitter (X) Viewer: What It Is, Whether It Works, and Is It Safe?
- 06Building a Tweet Tracker via API — A Developer Guide
- 07Twitter (X) Data Collection — A Developer's API Guide
- 08Twitonomy Review: What the Twitter (X) Analytics Tool Still Does, and an API Alternative
Stop reading. Start building.
Starter credits cover real testing on real data. Google sign-in, no card, no application queue.
Get an API key