Skip to content

Build a Twitter dataset for machine learning

A Twitter dataset for machine learning is a set of tweets with their text, dates and authors, either downloaded from a public archive such as Sentiment140 (1.6 million tweets from 2009) or collected fresh through an API.

Short answer: Public sets are free but years old, and most ship as tweet IDs you have to look up again before any text appears. A fresh set costs $5.00 per 1,000 posts on the official X API. To build one, use Sorsa API: POST /search-tweets collects topic tweets from $0.07 per 1,000, and 100 free requests cover a 2,000-tweet test pull.

Endpoint
POST /search-tweets
Page size
about 20 tweets per request
Price
from $0.07 per 1,000

#Where Twitter datasets come from

Every Twitter (X) dataset starts in one of two places: a collection someone already published, or tweets you collect yourself through an API. A published set is free and loads in minutes. A collection of your own matches your topic, language, dates and labels, because you choose them; our guide on how to scrape Twitter compares every way to pull tweets in 2026.

The best-known public sets were gathered between 2009 and 2022, while Twitter still gave researchers free access to its API, and several no longer hand out tweet text at all.

DatasetSizeCollectedWhat you get today
Sentiment1401.6M tweets, 498 for testing2009Text with positive or negative labels taken from emoticons
TweetEval (Cardiff NLP)7 classification tasksBenchmark from 2020Text and labels for sentiment, emotion, irony, hate, offensive language, emoji and stance
COVID-19-TweetIDsOver 1B tweetsFrom January 2020Tweet IDs only
ArchiveTeam Twitter archiveAbout 16B records2011 to 2022Raw JSON, no labels
SNAP Twitter7 (Stanford)476M tweets2009Nothing: withdrawn at Twitter's request
  • Sentiment140

    Size
    1.6M tweets, 498 for testing
    Collected
    2009
    What you get today
    Text with positive or negative labels taken from emoticons
  • TweetEval (Cardiff NLP)

    Size
    7 classification tasks
    Collected
    Benchmark from 2020
    What you get today
    Text and labels for sentiment, emotion, irony, hate, offensive language, emoji and stance
  • COVID-19-TweetIDs

    Size
    Over 1B tweets
    Collected
    From January 2020
    What you get today
    Tweet IDs only
  • ArchiveTeam Twitter archive

    Size
    About 16B records
    Collected
    2011 to 2022
    What you get today
    Raw JSON, no labels
  • SNAP Twitter7 (Stanford)

    Size
    476M tweets
    Collected
    2009
    What you get today
    Nothing: withdrawn at Twitter's request

For a generic sentiment or emotion baseline, one of the labeled sets is the fastest start. The community-kept awesome-twitter-data list (opens in a new tab) tracks hundreds more and marks which ones still share text. For anything specific, such as reactions to your product, an event from this year, a language the archives skip or a label scheme of your own, you build the dataset.

#The catch with public Twitter datasets

Public Twitter datasets share four problems: they are old, most of them hold only IDs, those IDs decay every year, and their labels were made for someone else's task.

  • Old text. Every tweet in Sentiment140 is 140 characters or shorter, because Twitter only raised the limit to 280 in November 2017. A model trained on 2009 language meets 2026 slang, topics and post lengths it has never seen.
  • IDs instead of text. A dataset shared with others usually carries only post IDs and user IDs, so many research sets, the big COVID-19 collections among them, are lists of numbers. You get the text back by rehydrating the IDs: looking each one up again through an API.
  • Decay. A tweet that was deleted, or whose author went private or got suspended, no longer comes back. The rows you lose are not random, so the rebuilt set leans toward accounts that are still active.
  • Borrowed labels. Sentiment140's labels came from emoticons, not from people reading the tweets, and benchmark labels answer the benchmark's question, not yours.

For a model that has to understand this year's posts, that leaves one route: a fresh set, cleaned and labeled for one task.

#How to build a fresh Twitter dataset with Sorsa API

For a fresh dataset, use Sorsa API, a Twitter (X) API and scraper: one API key, X's search operators back to 2006, about 20 tweets per request from $0.07 per 1,000, and 20 requests per second on every plan, so a 1M-tweet topic set takes about 42 minutes to collect. Every tweet arrives with its author's full profile at no extra cost, and nothing touches an X account of yours.

1 Collect topic tweets, reply threads or a list of IDs POST /search-tweets IDs: POST /tweet-info-bulk 2 Clean drop retweets, copies and near-duplicates lang, made_with_ai handles to @user, links to http 3 Label a gold set by hand, the rest pre-labeled label_source human, model or query 4 Split whole threads, newest weeks as the test conversation_id_str then cut by created_at 5 Save one record per line, with a dataset card train.jsonl, test.jsonl queries, dates, recovery rate
Collection is one API call in a loop. Most of the work that decides model quality happens after it: cleaning, labels and an honest split.

Pick the endpoint by what defines your dataset:

  1. Get a key. Sign up and copy your API key from the dashboard. The first 100 requests are free, with no card.

  2. Write the query. Search operators shape the dataset before you pay for it: lang:en keeps one language, -filter:retweets drops copies, since: and until: fix the window. Test a query by hand in the free Twitter Advanced Search tool first.

  3. Send a test request. One request returns about 20 tweets and a cursor to the next page.

    Terminal
    curl -X POST "https://api.sorsa.io/v3/search-tweets" \
      -H "ApiKey: YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"order": "latest",
           "query": "crashing lang:en -filter:retweets since:2026-10-01"}'
    
    Response200 OK
    {
      "tweets": [
        {
          "id": "2115807747265175863",
          "full_text": "The new beta keeps crashing on launch for me",
          "created_at": "2026-10-29T14:07:31Z",
          "lang": "en",
          "likes_count": 12,
          "reply_count": 3,
          "retweet_count": 0,
          "conversation_id_str": "2115807747265175863",
          "in_reply_to_tweet_id": null,
          "is_reply": false,
          "made_with_ai": false,
          "user": {
            "id": "1710221540156513285",
            "username": "maya_trains_models",
            "followers_count": 412,
            "created_at": "2023-10-06T09:12:44Z",
            "verified": false
          }
        }
      ],
      "next_cursor": "DAADDAABCgABHV5mIq0VsAoKAAIdXmUz8Kqg"
    }
    
  4. Page through and save raw JSONL. Send next_cursor back until it comes back empty, and write every tweet untouched, one JSON object per line. Cleaning runs later on a copy, so you never pay to collect the same tweets twice.

    Python
    import json
    import time
    
    import requests
    
    URL = "https://api.sorsa.io/v3/search-tweets"
    HEADERS = {"ApiKey": "YOUR_API_KEY"}
    QUERY = "crashing lang:en -filter:retweets since:2026-10-01"
    
    
    def collect(query, path, limit=50_000):
        body = {"query": query, "order": "latest"}
        saved = 0
        with open(path, "a", encoding="utf-8") as out:
            while saved < limit:
                r = requests.post(URL, headers=HEADERS, json=body, timeout=30)
                if r.status_code == 429:  # over 20 requests per second
                    time.sleep(1)
                    continue
                r.raise_for_status()  # 403: no key, or no requests left
                page = r.json()
                for tweet in page.get("tweets", []):
                    tweet["_query"] = query
                    out.write(json.dumps(tweet, ensure_ascii=False) + "\n")
                    saved += 1
                if not page.get("next_cursor"):
                    break
                body["next_cursor"] = page["next_cursor"]
        return saved
    
    
    print(collect(QUERY, "raw_tweets.jsonl"), "tweets saved")
    

Run one query per class or topic, each into its own file, and the per-file counts tell you at once whether a class is too thin. To go further back than this year, the same endpoint reaches every public tweet since 2006; our guide to historical Twitter data covers date slicing for large backfills.

#Rehydrate a tweet ID dataset

To rebuild a published ID dataset, send its tweet IDs to POST /tweet-info-bulk (opens in a new tab) on Sorsa API, up to 100 per request, and keep whatever comes back. A list of 1M IDs costs from $14 and takes about 8 minutes at 20 requests per second; the same 1M posts cost $5,000 in post reads on the official X API.

Python
import json

import requests

URL = "https://api.sorsa.io/v3/tweet-info-bulk"
HEADERS = {"ApiKey": "YOUR_API_KEY"}

with open("tweet_ids.txt", encoding="utf-8") as f:
    ids = list(dict.fromkeys(x.strip() for x in f if x.strip()))

found = set()
with open("rehydrated.jsonl", "w", encoding="utf-8") as out:
    for i in range(0, len(ids), 100):
        body = {"tweet_links": ids[i:i + 100]}
        r = requests.post(URL, headers=HEADERS, json=body, timeout=30)
        r.raise_for_status()
        for tweet in r.json().get("tweets", []):
            found.add(tweet["id"])
            out.write(json.dumps(tweet, ensure_ascii=False) + "\n")

missing = [x for x in ids if x not in found]
print(f"recovered {len(found)} of {len(ids)}; {len(missing)} gone")

Report the recovery rate with the dataset: how many IDs came back, out of how many, on which date. Two datasets rebuilt from the same IDs a year apart are different datasets, and a reader of your results needs to know which one you trained on.

Good to know

Keep tweet IDs as text from start to finish. An ID has 19 digits, while a spreadsheet keeps 15 significant digits and a JavaScript number about 16, so an ID that passes through Excel or JSON.parse comes back rounded and matches no tweet. Read ID files as strings, and save them in quotes.

#Clean and deduplicate the tweets

Clean a tweet dataset in four passes: drop retweets and exact duplicates by ID, collapse near-duplicate text, filter by language and author, and swap handles and links for placeholders. Each pass uses fields that come back in every tweet.

FieldWhat it does in a dataset
idThe primary key; store it as a string
full_textThe text you clean, label and train on
langKeeps each split in one language
conversation_id_strGroups a thread, so the split can keep it together
in_reply_to_tweet_idLinks a reply to its parent for conversation pairs
made_with_aiMarks posts X labels as made with AI; leave them out of human-text sets
user.followers_count, user.created_atAuthor signals for spam and bot filters
likes_count, reply_countA quality floor: posts nobody engaged with are often spam
  • id

    What it does in a dataset
    The primary key; store it as a string
  • full_text

    What it does in a dataset
    The text you clean, label and train on
  • lang

    What it does in a dataset
    Keeps each split in one language
  • conversation_id_str

    What it does in a dataset
    Groups a thread, so the split can keep it together
  • in_reply_to_tweet_id

    What it does in a dataset
    Links a reply to its parent for conversation pairs
  • made_with_ai

    What it does in a dataset
    Marks posts X labels as made with AI; leave them out of human-text sets
  • user.followers_count, user.created_at

    What it does in a dataset
    Author signals for spam and bot filters
  • likes_count, reply_count

    What it does in a dataset
    A quality floor: posts nobody engaged with are often spam

Near-duplicates are the pass most pipelines skip. Retweets with a comment, copied posts, bot campaigns and the same complaint pasted by ten accounts all survive an ID check, and they inflate whatever they say. Normalize the text first (lowercase, no links, no handles, no punctuation), then compare word shingles: two posts that share most of their three-word runs say the same thing. The checker below runs that test on whatever you paste.

Tweets
7
Unique after cleaning
4
Duplicates removed
3
  1. KeptThe new beta keeps crashing on launch :( https://t.co/k2Vq81

    the new beta keeps crashing on launch

    • Same textRT @priya_ships_features: The new beta keeps crashing on launch :(
    • 71% matchthe new beta keeps crashing on launch for me!!
  2. KeptLoving the new dark mode, finally easy on the eyes

    loving the new dark mode finally easy on the eyes

    • Same text@nadia_tests_builds loving the new dark mode... finally easy on the eyes https://t.co/x9Lm2
  3. KeptBattery life on the beta is noticeably better this week

    battery life on the beta is noticeably better this week

  4. KeptDoes anyone else get logged out after the update?

    does anyone else get logged out after the update

Paste one tweet per line. Lines whose cleaned text shares at least 70% of its three-word runs fall into one group, and only the first line of each group stays.

For text the model will read, replace each handle with @user and each link with http, the convention Cardiff NLP's tweet models use. The model then learns how people write, not which accounts and links appear most often. Keep the original full_text beside the cleaned one, so you can change the cleaning later without collecting again.

Python
import hashlib
import json
import re


def norm(text):
    text = re.sub(r"https?://\S+", " ", text.lower())
    text = re.sub(r"^rt @\w+:?|@\w+", " ", text)
    text = re.sub(r"[^\w\s#]", " ", text)
    return " ".join(text.split())


seen, rows = set(), []
for line in open("raw_tweets.jsonl", encoding="utf-8"):
    t = json.loads(line)
    if t.get("retweeted_status") or t.get("made_with_ai"):
        continue
    clean = norm(t["full_text"])
    key = hashlib.sha1(clean.encode()).hexdigest()
    if key in seen or len(clean.split()) < 4:
        continue
    seen.add(key)
    rows.append(t)

The hash catches exact copies after normalization; for fuzzy matches at the scale of millions, MinHash with locality-sensitive hashing does the shingle comparison without comparing every pair.

#Label the dataset

Label a tweet dataset in two layers: a gold set of a few hundred tweets that people label by hand, and the bulk pre-labeled by a model and checked by a person where the model is unsure. The gold set never goes into training; it is the test that tells you whether the bulk labels are good enough.

Write the label rules down before anyone starts, with an example of each class and of the hard cases: sarcasm, questions, quotes of someone else's opinion. Two people labeling the same 100 tweets and comparing results show where the rules are vague. Store with each label who or what produced it (human, model, query), so you can drop or reweigh the weak ones later.

A search query can act as a weak label: tweets collected for "keeps crashing" lean negative. Use that only to bootstrap, because a query label teaches the model the query's words, not the sentiment.

#Split train and test without leaking

Split a tweet dataset by conversation and by time, never row by row: hold out the newest weeks as the test set, and keep every thread on one side of the line. A random split puts a post and its replies, or two copies of the same text, into both train and test, and the test score then measures memory instead of skill.

Train Test Tweets in date order, older to newer Random split each row goes to train or test by chance One reply thread lands on both sides: the test has already been seen Grouped by thread, cut by date whole threads stay together, the newest weeks are the test The thread stays in train The newest weeks
Group by conversation_id_str, then cut by date. The test set then looks like next month's tweets, which is what the model will meet.
Python
import hashlib

rows.sort(key=lambda t: t["created_at"])
cut = rows[int(len(rows) * 0.9)]["created_at"]  # newest 10% = test
test = [t for t in rows if t["created_at"] >= cut]

threads = {t.get("conversation_id_str") for t in test}
rest = [t for t in rows if t["created_at"] < cut
        and t.get("conversation_id_str") not in threads]


def bucket(t):
    key = t.get("conversation_id_str") or t["id"]
    return int(hashlib.md5(key.encode()).hexdigest(), 16) % 10


train = [t for t in rest if bucket(t) != 0]
valid = [t for t in rest if bucket(t) == 0]

For tasks about accounts rather than posts, such as Twitter bot detection, group by user.id instead, so no account appears on both sides.

#Save it in the format your model reads

Save a tweet dataset as JSONL, one record per line, with the tweet ID in every record. JSONL streams, appends and loads straight into pandas, the Hugging Face datasets library and most training scripts. Which fields go in each line depends on the job.

JobWhat each JSONL line holds
Classifier or topic modelid, cleaned text, label, label_source, lang, created_at
Evaluation set for a model or a promptid, text, the expected answer, and the rule it tests
RAG index for retrievalid, text, the post URL, created_at and the author ID, so answers can cite the post
  • Classifier or topic model

    What each JSONL line holds
    id, cleaned text, label, label_source, lang, created_at
  • Evaluation set for a model or a prompt

    What each JSONL line holds
    id, text, the expected answer, and the rule it tests
  • RAG index for retrieval

    What each JSONL line holds
    id, text, the post URL, created_at and the author ID, so answers can cite the post
One classification record
{
  "id": "2115807747265175863",
  "text": "the new beta keeps crashing on launch for me",
  "label": "negative",
  "label_source": "human",
  "lang": "en",
  "created_at": "2026-10-29T14:07:31Z",
  "split": "test"
}

Ship a dataset card with the files: the queries and dates, the day you collected or rehydrated, the recovery rate, how labels were made, what was filtered out and what the set should not be used for. Hugging Face's dataset cards follow the same outline, and the next person to train on your data, often you in six months, needs every line of it.

#What a Twitter dataset costs

A 1M-tweet dataset costs $5,000 in post reads on the official X API, against from $70 on Sorsa API by search or from $14 when you rehydrate known IDs. Switch the size to see the totals for a fresh collection by search.

Dataset size

Official X API

$5,000

for 1M tweets

$0.005 per post

Sorsa API

from$70

for 1M tweets

from $0.07 per 1,000

Two limits on the official X API change the math for datasets. Its pay-per-use plan stops at 3M post reads a month, so a 10M-tweet set needs an Enterprise contract, which X's price list (opens in a new tab) points to above that cap. And from November 22, 2026, X also bills the author profile that comes back with each post as a user read, $0.010 more per post (X's notice (opens in a new tab)). On Sorsa API the author's full profile is part of every tweet, and failed requests are never charged.

Time is the other half of the budget. At 20 requests per second, Sorsa API collects about 400 tweets a second by search and up to 2,000 a second by ID, so 1M fresh tweets take about 42 minutes and a 10M-tweet set about 7 hours on one key.

#How to share and train on a tweet dataset

Public research datasets ship as tweet and user IDs, and anyone with the IDs gets the full text back through an API: Sorsa API rehydrates 1M IDs in about 8 minutes.

  • Sharing. Publish the tweet IDs and user IDs with a rehydration script, the way public research datasets do; POST /tweet-info-bulk turns 100 IDs back into full posts per request.
  • Training. Keep the full text you collect for your own models: classifiers, topic models, evaluation sets, retrieval indexes and language-model fine-tunes are the common jobs for tweet datasets.
  • People. Replace handles before training, keep no profiles of individuals, and remove tweets their authors deleted when you refresh the set.

Sorsa API returns public data only and never posts, likes or follows. Universities with a research project can also ask about academic access to Sorsa API.

#Start your dataset with a test pull

Public Twitter datasets give you a baseline; a fresh one gives you your own topic, your own dates and labels that fit your task. Build it with Sorsa API: from $0.07 per 1,000 tweets by search and from $0.014 per 1,000 by ID, at 20 requests per second, with the author's profile in every tweet. Your 100 free requests collect the first 2,000 tweets, enough to test the query, the cleaning and the split before you pay for anything.

#FAQ

What is a Twitter dataset?

A Twitter dataset is a collection of posts from X, formerly Twitter, saved in a file for analysis or machine learning: usually the tweet ID, text, date, language, engagement counts and author fields, sometimes with labels such as sentiment. Many published datasets hold only tweet IDs, the research norm for sharing, so the text has to be looked up again.

Where can I download a Twitter dataset for free?

Kaggle, Hugging Face and the awesome-twitter-data list on GitHub host hundreds of free Twitter datasets, among them Sentiment140 with 1.6M labeled tweets from 2009 and the TweetEval benchmark. Most were collected before 2023, and many share only tweet IDs, which you rehydrate through an API before the text exists. Sorsa API rehydrates 100 IDs per request from $0.014 per 1,000.

Which API should I use to build a Twitter dataset?

Use Sorsa API to build a Twitter dataset: search with X's operators back to 2006 returns about 20 tweets per request from $0.07 per 1,000, rehydration takes 100 IDs per request from $0.014 per 1,000, and every plan runs 20 requests per second with one API key, no developer account and no OAuth. The official X API charges $5.00 per 1,000 posts and stops at 3M post reads a month.

Can I use Twitter data to train an LLM?

Yes. Public X conversations are a common source of training and fine-tuning data, and tweet datasets also feed sentiment and topic classifiers, evaluation sets and retrieval (RAG) indexes that let an assistant cite real posts. Sorsa API collects them at 20 requests per second: about 20 tweets per search request from $0.07 per 1,000, or 100 by ID from $0.014 per 1,000.

How long does it take to collect 1 million tweets?

On Sorsa API, 1 million tweets take about 42 minutes by search and about 8 minutes by tweet ID, at 20 requests per second on one key. Cleaning and deduplication then remove part of them, so measure the loss on a test pull and collect that much more than the size you want to end with.

#How we checked

On November 2, 2026 we read X's pay-per-use price list on docs.x.com, and checked the dataset pages of Sentiment140, TweetEval and SNAP's Twitter7. Every Sorsa API endpoint, field and page size on this page matches the live API reference of the same day; the response shown follows that reference's schema, and we did not run the code samples against the API. Collection times are arithmetic from the page sizes and the rate limit of 20 requests per second.

By the Sorsa API team. We build and run Sorsa API, the read-only Twitter/X API and scraper, in production since 2022 (formerly TweetScout API) with 6B+ requests served. About Sorsa API

What changed
  • Rewritten from scratch: public datasets and what is left of them, building a fresh set, rehydrating IDs, cleaning with near-duplicate checks, leak-free splits, formats, costs and X's current rules for sharing and training.
  • First published.