Nitter Is Almost Gone. Here's What Still Returns Public Tweets in 2026 (Tested)
Nine Nitter instances probed, one answered. Plus the syndication, oEmbed and server-rendered surfaces that still work, with byte counts and a date.
12 min read · 20 Aug 2026
Your Nitter instance stopped returning tweets, or the one you bookmarked is now a Cloudflare challenge, and you are trying to work out whether the whole approach is dead or whether you just picked the wrong host.
Both, partly. Here is what we actually got back when we probed it, with byte counts.
Every result below was produced on 2026-08-20, with curl, from a single IP: AS133982, Excitel Broadband, New Delhi, India — a consumer broadband line. That last detail is not decoration. It is the single largest variable in these results, and the section on why is further down. If you run these from a cloud VM you will get different answers, and mostly worse ones.
X is one platform in a broader survey; the same probes run against nine other platforms are collected in what each social platform returns without an API key.
What actually happened to Nitter
The popular version — "X killed guest tokens in early 2024 and Nitter died" — is the first half of the story and it makes the second half incomprehensible.
Nitter worked by registering guest accounts through an internal Twitter API and reading with their tokens. X stopped issuing them in early 2024 and the free multi-instance ecosystem went with them. But the project came back, and how it came back is the thing that explains every result in this post.
On 2025-02-05 the maintainer merged a pull request titled "Replace guest tokens with account sessions," whose entire description reads: "Nitter is officially back, but hosting an instance is nowhere near as easy as it used to be." The linked wiki page is blunter still:
Running a large public instance is difficult, and is not feasible with a small amount of accounts. Nitter requires real accounts to authenticate API requests.
Sessions now go in a sessions.jsonl file, one JSON object per line, each holding an auth_token and a ct0 — logged-in X account cookies.
Everything follows from that. An operator is spending real accounts, which X will suspend if they are used hard enough, so instances defend themselves against bots with the same ferocity as the platform they proxy. That is why six of the nine instances below served us a challenge page. It is also why RSS keeps getting switched off, and why "just self-host Nitter" is no longer a serious answer to anything.
It has a legal dimension too, and it is not a small one. Reading X while logged into an account you agreed terms for is a materially different posture from reading a public page while logged out — the distinction that most of the case law turns on. Nitter today sits on the wrong side of it by design. Everything else in this post sits on the other side.
The project itself is very much alive: zedeus/nitter was pushed to on 2026-08-19, the day before these tests, with 13,460 stars and 163 open issues. It is not archived. What ended was the era where anyone could run an instance for everyone.
The nine instances, probed
The canonical health page at status.d420.de listed nine instances at the time of testing. We requested https://<host>/NASA from each, followed redirects, and counted how many timeline-item blocks came back — Nitter's markup for one tweet.
| Instance | Health page said | We got | Tweets |
|---|---|---|---|
| nitter.tiekoetter.com 🇩🇪 | Healthy | 200, 85,034 bytes | 20 |
| nitter.privacyredirect.com 🇫🇮 | Healthy | 200, 24,551 bytes — "Making sure you're not a bot!" | 0 |
| xcancel.com 🇺🇸 | Healthy | 200, 11,354 bytes — "Verifying your browser…" | 0 |
| nitter.net 🇳🇱 | Healthy | 200, 0 bytes | 0 |
| nitter.poast.org 🇺🇸 | Healthy | 403, 19,973 bytes — "Verifying your browser | Nitter" | 0 |
| nitter.catsarch.com 🇺🇸/🇩🇪 | Healthy | 403, 5,699 bytes | 0 |
| lightbrd.com 🇹🇷 | Healthy | 403, 5,244 bytes — Cloudflare "Just a moment..." | 0 |
| nitter.space 🇺🇸 | Healthy | 403, 5,244 bytes — Cloudflare | 0 |
| nuku.trabun.org 🇨🇱 | Healthy | 403, 5,396 bytes — Cloudflare | 0 |
Read the first two columns together, because the gap between them is the interesting part. Every instance was marked healthy. One returned tweets.
That is not the status page lying. It is a monitor that asks "did the host answer?" and gets a defensible yes from a Cloudflare interstitial, from a JavaScript bot challenge, and — in nitter.net's case — from a 200 response with a completely empty body. A health check that keys on status codes cannot see any of this, which is a specific instance of a more general problem worth understanding.
It is also not a claim that eight instances are broken. Six of the nine served a bot challenge, not an outage. Open xcancel.com in a real browser and you will very likely get your tweets, because you have a JavaScript engine, a plausible fingerprint, and you are one person. The distinction that matters here is between "a human can read this" and "a program can read this," and those have come apart almost completely.
The one that worked, worked properly:
nitter.tiekoetter.com |
Result |
|---|---|
/NASA |
200, 85,034 bytes, 20 tweets |
/NASA/status/<id> |
200, 67,939 bytes |
/search?f=tweets&q=mars |
200, 58,771 bytes, 19 results across many accounts |
/NASA/rss |
403 — <span>RSS feed is disabled</span> |
Working search is genuinely notable, because nothing else in this post has it. RSS being switched off is the norm now rather than the exception: an RSS reader polling every fifteen minutes is a load pattern instance operators cannot afford any more.
One instance is not an architecture. If your plan is "point at tiekoetter," your plan has a single point of failure run by a volunteer who did not agree to carry you.
The syndication endpoints, and the parameter that does nothing
X keeps one public surface alive on purpose: the infrastructure behind embedded tweets. Publishers depend on it, so it survives rounds of lockdown that kill everything else.
One tweet
GET https://cdn.syndication.twimg.com/tweet-result?id=<id>&lang=en&token=<anything>
That returns clean JSON. For tweet 20 (Jack Dorsey's first) it is 920 bytes; for a NASA post with photos it was 2,791. The payload carries text, created_at, entities, photos, mediaDetails, favorite_count, conversation_count, edit_control, and a thin user object — id_str, name, screen_name, is_blue_verified, profile_image_url_https. No follower count, no retweet count.
Now the part worth the price of the post. The token parameter is required but not validated.
| Request | Response |
|---|---|
?id=20&lang=en (no token) |
200, 2 bytes: {} |
?id=20&lang=en&token= (empty) |
200, 2 bytes: {} |
?id=20&lang=en&token=a |
200, 920 bytes, full tweet |
?id=20&lang=en&token=zzzzzz |
200, 920 bytes, full tweet |
?id=20&lang=en&token=6dq1a2xwd93 (the real one) |
200, 920 bytes, full tweet |
We repeated this against a tweet posted the previous day, in case tweet 20 was some ancient cached artefact. Same result: no token gives {}, token=a gives the whole thing.
X's own embed script does compute a real value — it is a cheap cache-buster derived from the tweet id, not a signature or a secret:
const token = id => ((Number(id) / 1e15) * Math.PI).toString(36).replace(/(0+|\.)/g, '');
// token('20') === '6dq1a2xwd93'
Compute it properly anyway. It costs you one line, it is what a real embed sends, and the day X starts checking the value is the day the lazy version silently starts returning {} — with a 200, and no error, and nothing in your logs.
Which is the other thing to take from that table. The failure mode for a missing token is not a 400. It is a successful response containing an empty object, and if your parser reads {} as "this tweet has no text" you will ship that conclusion to your users. Two bytes.
Some other measured behaviour: a tweet id that does not exist returns 404 with an HTML error page, so real absence and a missing token are distinguishable if you check; twelve requests back to back all succeeded, because it is a CDN and behaves like one; and Access-Control-Allow-Origin is pinned to https://platform.twitter.com, so you cannot call this from a browser on your own origin.
A profile timeline
GET https://syndication.twitter.com/srv/timeline-profile/screen-name/<handle>
This is the embedded-timeline widget's server-rendered page, with the data in a __NEXT_DATA__ script tag. Eight handles, eight 200s, 115 KB to 637 KB each, 20 tweets every time.
It is also, unexpectedly, the richest thing on this list. Each tweet carries full_text, favorite_count, retweet_count, quote_count, reply_count, extended_entities and a permalink. And the author object attached to those tweets is the full legacy user shape:
followers_count: 92333972 friends_count: 118
statuses_count: 74167 media_count: 28083
listed_count: 96985 created_at: "Wed Dec 19 20:20:32 +0000 2007"
description: "Making the seemingly impossible, possible. ✨"
profile_banner_url: <present>
That is strictly more per tweet than tweet-result gives you, including the follower count that the old widgets/followbutton/info.json endpoint used to serve. For the record, we checked that one too: it now answers HTTP 200 with a zero-byte body for every handle. Gone, not merely undocumented.
It is a rendering surface, not an API. X has restricted it before and will again. Treat every field as optional and fail soft.
oEmbed, if you only need the text
GET https://publish.twitter.com/oembed?url=https://x.com/jack/status/20
200, 521 bytes, no auth, no token. You get author_name, author_url, and html — a <blockquote> containing the tweet text and a dated permalink. No counts, no structured timestamp, no media URLs.
It also sets Access-Control-Allow-Origin to whatever origin you send, so unlike the syndication CDN this one is callable from a browser. If your requirement is "render the tweet a user pasted," this is the whole answer and you should stop reading.
x.com itself, which is no longer a login wall
This surprised us enough that we checked it three times.
https://x.com/NASA, plain curl, no cookies, no user-agent games: 200, 282,546 bytes, and the HTML contains six tweets with full_text, favorite_count, retweet_count and reply_count serialised into an inline Relay store, plus schema.org microdata per tweet:
<meta content="2026-08-19T22:03:05.000Z" itemProp="datePublished"/>
<meta content="https://x.com/NASA/status/2090197889947451524" itemProp="url"/>
The single-status page does the same and additionally puts the full tweet text in <title> and the Open Graph tags.
The caveat is a big one: this is the result most sensitive to where you ask from. Our own datacenter probes of X profile surfaces get rate-limited quickly, which is why our Twitter profile endpoints are documented as needing residential IPs. A broadband line in Delhi looks like a person; a Hetzner box does not. If you take one number from this post, do not take this one.
The third-party JSON APIs
api.fxtwitter.com and api.vxtwitter.com — the services behind the link-fixer bots — both answer with clean JSON and no auth, at 1,246 and 722 bytes for tweet 20.
They are caches, and the cache is visible in the response. vxtwitter reported fetched_on: 1786753203 — 2026-08-15, five days before we asked — and gave the like count as 308,710 where syndication, live, said 308,927. Neither is wrong. One is older than you probably assumed, and nothing in your code will notice.
They are also somebody else's free service with somebody else's uptime. Fine for a Discord bot, not for a pipeline. The public RSSHub instance at rsshub.app was worse: 403 on the root, 404 on /twitter/user/NASA.
The official API, and what X actually charges
X's own documentation now says, verbatim: "The X API uses pay-per-usage pricing. No subscriptions—pay only for what you use." Credits bought upfront, deducted as you go. The docs point at a separate pricing page for the numbers, but the numbers themselves come from an endpoint the docs' own cost calculator fetches, and that endpoint needs no authentication:
GET https://console.x.com/api/credits/pricing
200, 643 bytes, JSON. As of 2026-08-20 it reports, in US dollars per unit:
| Unit | Price |
|---|---|
| Post read | $0.005 |
| User read | $0.01 |
| Owned read (your own app's user) | $0.001 |
| Trends | $0.01 |
| Post create | $0.015 |
| Post create containing a link | $0.20 |
| Like / Mute / Block read | $0.001 |
Do the arithmetic before you decide the free routes are not worth the trouble: 1,000 tweet reads is $5.00, and each resource returned counts, so a timeline call that hands you 20 posts costs twenty units, not one. Above the self-serve ceiling the only option is a custom Enterprise contract.
That is not a scandal — it is a price, and for a business that needs guaranteed access and a support contact it is a reasonable one. It is just a very different number from zero, and worth knowing before you architect around it.
Three difficulty levels, not one problem
"How do I read tweets without the API" is three questions, and they are not close in difficulty.
One tweet, by id. Solved several ways over, from any IP: syndication, oEmbed, fxtwitter, the <title> of the status page. Pick one, add a fallback.
A profile's recent posts. Solvable with caveats. timeline-profile gives 20 with full metrics, x.com HTML gives six. Both depend on where you ask from, and both are rendering surfaces that can change without notice.
Search, historical timelines, anything past the most recent 20. Not solved by anything here. One volunteer-run Nitter instance had working search on the day we tested; that is the whole list. If you need this, you need the paid API or a different requirement.
Why the IP decides so much of this
The pattern across the whole set is consistent: the endpoints are not the variable, the origin is. A consumer broadband IP gets a server-rendered x.com profile; a datacenter IP in the same second gets rate-limited. Same request, same endpoint. One address belongs to an ASN with millions of humans behind it and the other belongs to a rack.
If your results do not match ours, check the ASN before you check your code — and before you spend money on the fix, what a residential proxy actually buys you is worth ten minutes, because a good share of what people buy them for does not need them.
What we could not make work
Publishing this bit is the point of running the tests.
- RSS from any Nitter instance. tiekoetter returns 403 and "RSS feed is disabled." The others never got far enough to ask.
- Anything beyond 20 tweets from a syndication timeline. There is no cursor we could find.
- Retweet counts and follower counts from
tweet-result. Both are present in the timeline payload and absent from the single-tweet one. We do not know why. widgets/followbutton/info.json. 200, zero bytes, every handle.
The short version
- Nitter now requires real logged-in X accounts, per its own wiki. That is why instances are scarce, defended by Cloudflare, and legally a different proposition from everything else here.
- One of nine listed instances answered a program on 2026-08-20 — and all nine were marked healthy, because a monitor that reads status codes cannot see a challenge page or an empty body.
- For a single tweet, the syndication CDN is the answer, and its
tokenparameter must be present but is not checked. Compute the real one anyway. - For a profile,
srv/timeline-profileis richer than the single-tweet endpoint — 20 posts with retweet, quote and reply counts, plus a full user object with followers. - Missing-parameter failures here return
{}with a 200, not an error. Count bytes. - The IP you ask from matters more than the endpoint you ask.
Re-run these yourself before you rely on them; every command in this post is one line of curl. We will re-date this page when we re-test it.