{
  "version": "https://jsonfeed.org/version/1.1",
  "title": "tgAtlas",
  "home_page_url": "https://tgatlas.org/",
  "feed_url": "https://tgatlas.org/feed.json",
  "description": "Every public Telegram channel worth knowing, measured weekly",
  "items": [
    {
      "id": "https://tgatlas.org/blog/telethon-moved-to-codeberg",
      "url": "https://tgatlas.org/blog/telethon-moved-to-codeberg",
      "title": "Telethon is alive on Codeberg. Its GitHub repo has been archived since February",
      "summary": "Telethon did not die. It moved — and the GitHub page every tutorial still links to has been archived since February.",
      "content_html": "\n<p class=\"prose\">Here is how most of us decide whether a library is safe to depend on: open its GitHub page, look at the last commit, glance at open issues, check the stars. Thirty seconds, and you have an answer.</p>\n<p class=\"prose\">For the two most used Telegram libraries in Python, that answer is now wrong.</p>\n\n<h2>What the numbers say</h2>\n<p class=\"prose\">Every figure below came from a public API on 16 September 2026 between 22:00 and 22:05 UTC, and every one of them was still standing when it was read again on 17 September.</p>\n<div class=\"tw\"><table>\n  <thead><tr><th scope=\"col\">Library</th><th scope=\"col\">What GitHub shows</th><th scope=\"col\">What is actually true</th></tr></thead>\n  <tbody>\n    <tr>\n      <th scope=\"row\">Telethon</th>\n      <td><code>LonamiWebs/Telethon</code><br><b>archived</b>, 12,057 stars, last push 21 Feb 2026, 1 open issue</td>\n      <td><code>Lonami/Telethon</code> on Codeberg<br>not archived, last commit 14 Sept 2026; PyPI serves 1.45.0, published 10 Sept 2026</td>\n    </tr>\n    <tr>\n      <th scope=\"row\">Pyrogram</th>\n      <td><code>pyrogram/pyrogram</code><br><b>archived</b>, 4,616 stars, last push 23 Dec 2024</td>\n      <td>PyPI still serves 2.0.106 from 30 Apr 2023; the documentation site carries no notice</td>\n    </tr>\n    <tr>\n      <th scope=\"row\">GramJS</th>\n      <td>repo archived</td>\n      <td>npm <code>telegram</code><br><b>891,746 downloads</b> between 13 Aug 2026 and 11 Sept 2026</td>\n    </tr>\n  </tbody>\n</table></div>\n<p class=\"prose\">Telethon did not die. It moved. The newest commit on Codeberg, dated 14 Sept 2026, is titled &laquo;Add missing parameters to friendly message methods&raquo;, and the release before it bumped the version to 1.45. Meanwhile the GitHub page that every tutorial links to has one open issue, because an archived repo cannot receive new ones.</p>\n<p class=\"prose\">Pyrogram is the opposite case. The repo is archived, the last release on PyPI is from April 2023, and the documentation site still reads like the project is current. Nothing tells a newcomer that the last three years happened.</p>\n<p class=\"prose\">GramJS is the strangest of the three. Nearly nine hundred thousand downloads a month, against an archived repository. Nobody is watching that dependency, and almost everyone writing Telegram code in JavaScript is standing on it.</p>\n\n<h2>Why this matters more than it looks</h2>\n<p class=\"prose\"><b>Your bug report goes nowhere.</b> An archived repo has issues disabled. If you hit a bug in Telethon today and file it where the stars are, you are filing it into a museum.</p>\n<p class=\"prose\"><b>New developers pick the wrong signal.</b> Someone starting a Telegram project this week searches, lands on the archived page, sees &laquo;archived&raquo; and concludes the whole ecosystem is dead. The opposite conclusion, &laquo;4,616 stars, must be fine&raquo;, is equally wrong for Pyrogram.</p>\n<p class=\"prose\"><b>Your assistant will be confidently wrong.</b> Coding assistants lean on what GitHub says. Ask one whether Telethon is maintained and you will get an answer built on an archived page.</p>\n<p class=\"prose\"><b>Tutorials keep pointing at the museum.</b> Search results for Telegram scraping are dominated by proxy vendors publishing the same walkthrough. None of them updated the link.</p>\n\n<h2>How to actually check whether a library is alive</h2>\n<p class=\"prose\">Four checks, none of which involve looking at stars.</p>\n<ol class=\"prose\">\n  <li><b>Ask the package registry, not the repo.</b> <code>pypi.org/pypi/&lt;name&gt;/json</code> gives you the upload time of the current version. For npm, <code>api.npmjs.org/downloads/point/last-month/&lt;name&gt;</code>. A release from last week beats a repo badge from last year.</li>\n  <li><b>Find where the maintainer actually works.</b> For Telethon that is Codeberg. The clue is usually in the README of the archived repo or in the maintainer's profile, and it is worth thirty seconds.</li>\n  <li><b>Read the download trend, not the total.</b> A package with steady monthly downloads and no releases is a risk you are taking on purpose. A package with both is fine.</li>\n  <li><b>Check whether issues are even open.</b> One open issue on a 12,000-star project usually means the tracker is closed, not that the bugs ran out.</li>\n</ol>\n\n<h2>The decision underneath all this</h2>\n<p class=\"prose\">There is a question hiding behind &laquo;which library should I use&raquo;, and it is worth asking out loud: do you need a Telegram <em>session</em> at all?</p>\n<p class=\"prose\">If you are building a bot that acts as itself, yes, and you should go with the library that is actually maintained, which today means Telethon from Codeberg, pinned to a version you tested.</p>\n<p class=\"prose\">If all you need is to read public channels, a session is a liability you took on by accident. It means a phone number, a session file that is worth stealing, FLOOD_WAIT timers you did not plan for, and an account that can be limited while you sleep. Plenty of projects carry that risk without ever needing the powers it buys them.</p>\n<p class=\"prose\">We built tgAtlas for the second case: <a href=\"https://tgatlas.org/docs\">public channels over HTTP</a>, no phone number, no session file. It reads posts with view and forward counts, the discussion threads under them, and channel metadata. If you are in the first case, ignore us and go star the Codeberg mirror, <code>codeberg.org/Lonami/Telethon</code>. It had 132 stars on 17 September 2026, and it is the one doing the work.</p>",
      "date_published": "2026-09-17T00:00:00.000Z"
    },
    {
      "id": "https://tgatlas.org/blog/comments-are-not-on-the-post",
      "url": "https://tgatlas.org/blog/comments-are-not-on-the-post",
      "title": "The comments on a Telegram post are not on the post",
      "summary": "They live in a linked discussion group with their own message ids, and a channel can have them on for one post and off for the next.",
      "content_html": "\n<p class=\"prose\">Telegram draws the comment thread under a channel post as if it were part of the post. It is not. The comments are messages in a separate discussion group, numbered in that group's own sequence, and a channel can have them switched on for one post and off for the next.</p>\n<p class=\"prose\">That is why so much Telegram tooling reports a view count and then goes quiet about the conversation underneath it. The chain is short, and every response below is live output captured on 16 September 2026.</p>\n\n<h2>The post tells you where its comments went</h2>\n<pre><code>curl '<b>https://telegram155.p.rapidapi.com/v1/peers/1892497462/history?limit=3</b>' \\\n  --header 'x-rapidapi-key: YOUR_KEY' \\\n  --header 'x-rapidapi-host: telegram155.p.rapidapi.com'</code></pre>\n<p class=\"prose\">Three consecutive posts from one channel, trimmed to the fields that matter:</p>\n<pre><code>{\n  \"count\": 613,\n  \"messages\": [\n    { \"id\": 4027, \"views\": 2508, \"forwards\": 21,\n      \"replies\": { \"comments\": true,  \"replies\": 23, \"channel_id\": 1965425283 } },\n    { \"id\": 4026, \"views\": 5886, \"forwards\": 30,\n      \"replies\": { \"comments\": true,  \"replies\": 27, \"channel_id\": 1965425283 } },\n    { \"id\": 4025, \"views\": 8021, \"forwards\": 93,\n      \"replies\": { \"comments\": false, \"replies\": 0,  \"channel_id\": 0 } }\n  ]\n}</code></pre>\n<p class=\"prose\">Post 4025 is the most viewed of the three and has no comments at all. Not zero comments — comments off, <code>channel_id</code> at <code>0</code>, nowhere to look. Posts 4027 and 4026 both point at <code>1965425283</code>: the discussion group linked to the channel, which is where their comments actually sit. One field, <code>replies.comments</code>, tells you which case you are in before you spend a second call.</p>\n\n<h2>Reading the thread</h2>\n<pre><code>curl '<b>https://telegram155.p.rapidapi.com/v1/peers/1892497462/messages/4027/replies?limit=5</b>' \\\n  --header 'x-rapidapi-key: YOUR_KEY' \\\n  --header 'x-rapidapi-host: telegram155.p.rapidapi.com'</code></pre>\n<pre><code>{\n  \"count\": 23,\n  \"next_page\": \"…\",\n  \"messages\": [\n    { \"id\": 404654, \"from_id\": { \"user_id\": 7780129537 }, \"message\": \"Yes\" },\n    { \"id\": 404651, \"from_id\": { \"user_id\": 1268179019 }, \"message\": \"Is that why the sudden crash?\" }\n  ],\n  \"users\": [ { \"id\": 7780129537, \"…\": \"…\" } ]\n}</code></pre>\n<p class=\"prose\">Look at the ids. Comment <code>404651</code> sits in the 400,000 range while the post is <code>4027</code>, because the comment is a message in the discussion group, not in the channel. <code>count</code> is the real total and <code>next_page</code> is a cursor, so walking 23 comments or 2,300 is the same loop. The <code>users</code> array comes back in the same response — who wrote what costs no extra call.</p>\n\n<h2>Both calls together</h2>\n<pre><code>import os, requests\n\nBASE = \"https://telegram155.p.rapidapi.com\"\nH = {\n    \"x-rapidapi-key\": os.environ[\"RAPIDAPI_KEY\"],\n    \"x-rapidapi-host\": \"telegram155.p.rapidapi.com\",\n}\nCHANNEL = 1892497462\n\nfeed = requests.get(f\"{BASE}/v1/peers/{CHANNEL}/history\",\n                    headers=H, params={\"limit\": 20}, timeout=10).json()\n\nfor post in feed[\"messages\"]:\n    r = post.get(\"replies\") or {}\n    if not r.get(\"comments\"):\n        continue\n\n    thread = requests.get(f\"{BASE}/v1/peers/{CHANNEL}/messages/{post['id']}/replies\",\n                          headers=H, params={\"limit\": 20}, timeout=10).json()\n\n    authors = {u[\"id\"]: u for u in thread.get(\"users\", [])}\n    views = post.get(\"views\") or 0\n    print(f\"post {post['id']}: {r['replies']} comments on {views:,} views \"\n          f\"({r['replies'] / views:.2%}), {len(authors)} distinct authors in the first page\")</code></pre>\n<p class=\"prose\">Twenty posts from that channel, captured minutes after the feed above:</p>\n<pre><code>post 4027: 23 comments on 2,510 views (0.92%), 19 distinct authors in the first page\npost 4026: 27 comments on 5,889 views (0.46%), 16 distinct authors in the first page\npost 4024: 64 comments on 8,048 views (0.80%), 12 distinct authors in the first page\npost 4023: 34 comments on 7,974 views (0.43%), 18 distinct authors in the first page\npost 4019: 42 comments on 20,458 views (0.21%), 17 distinct authors in the first page\npost 4017: 85 comments on 28,484 views (0.30%), 14 distinct authors in the first page</code></pre>\n<p class=\"prose\">The last column is the interesting one. Reach and conversation are separate signals, and here they move in opposite directions: post 4027 reached 2,510 people and 0.92% of them said something, while post 4019 reached eight times as many and got a quarter of the rate. Ranking these posts by views alone would have pointed you at the quieter audience.</p>\n\n<h2>The discussion root</h2>\n<p class=\"prose\">If you want the post as the discussion group itself sees it, there is a second endpoint: <code>GET /v1/peers/1892497462/messages/4027/discussion</code>. It returns the root message inside the group along with <code>max_id</code>, <code>unread_count</code> and the group chat objects — what you need to send a reader straight into the conversation instead of the channel.</p>\n\n<h2>Two things worth planning for</h2>\n<p class=\"prose\"><b>Comment spam is ordinary, and the signals to filter it ship with the data.</b> Of the five newest comments on post 4027, three carried links, one of them with Cyrillic lookalikes swapped in to slip past filters. The <code>users</code> array is where a filter starts: a missing username and a freshly minted numeric id are both cheap signals, and both arrive in the response you already paid for.</p>\n<p class=\"prose\"><b>Read <code>can_view_participants</code> before you enumerate.</b> A linked discussion group will usually let you list its participants where the broadcast channel will not, and the channel object carries the flag — so you know which of the two to ask, and you know it before you spend the call.</p>\n\n<p class=\"prose\">Three calls — feed, thread, discussion root — and the conversation under a channel's posts is yours to read. The free plan is 2,500 calls a month with no card — at three calls a conversation that is about 833 threads read before anything is paid for. <a href=\"https://tgatlas.org/quickstart\">Start with the quickstart</a>, or go straight to <a href=\"https://tgatlas.org/docs\">the reference</a>: 19 routes, one API key, no phone number.</p>",
      "date_published": "2026-09-16T00:00:00.000Z"
    },
    {
      "id": "https://tgatlas.org/blog/a-username-can-be-an-empty-string",
      "url": "https://tgatlas.org/blog/a-username-can-be-an-empty-string",
      "title": "The channel whose username is an empty string",
      "summary": "Ask for @durov and the username field comes back as \"\". Not null, not missing — the six real handles moved into an array, and only one of them belongs in a link.",
      "content_html": "\n<p class=\"prose\">Ask the API for <code>@durov</code> and read the <code>username</code> field. It comes back as <code>\"\"</code>.</p>\n<p class=\"prose\">Not null, not missing. An empty string, on the account of the person who founded Telegram. If your code does <code>chat.username or chat.id</code> and builds a link out of the result, you have just produced <code>t.me/1006503122</code>, which resolves to nothing for a human being.</p>\n<p class=\"prose\">Here is what is actually going on, and it is not a bug. Every response below is live output, captured on 16 September 2026.</p>\n\n<h2>A channel can hold more than one handle</h2>\n<p class=\"prose\">Telegram lets a chat own several public handles. When it does, the primary <code>username</code> field goes empty and the handles move into a <code>usernames</code> array, each entry carrying its own flags.</p>\n<pre><code>curl '<b>https://telegram155.p.rapidapi.com/v1/usernames/durov</b>' \\\n  --header 'x-rapidapi-key: YOUR_KEY' \\\n  --header 'x-rapidapi-host: telegram155.p.rapidapi.com'</code></pre>\n<pre><code>{\n  \"chats\": [\n    {\n      \"id\": 1006503122,\n      \"username\": \"\",\n      \"usernames\": [\n        { \"username\": \"durov\", \"active\": true, \"editable\": true,  \"flags\": 3 },\n        { \"username\": \"rove\",  \"active\": true, \"editable\": false, \"flags\": 2 },\n        { \"username\": \"paul\",  \"active\": true, \"editable\": false, \"flags\": 2 },\n        { \"username\": \"snow\",  \"active\": true, \"editable\": false, \"flags\": 2 },\n        { \"username\": \"feed\",  \"active\": true, \"editable\": false, \"flags\": 2 },\n        { \"username\": \"lean\",  \"active\": true, \"editable\": false, \"flags\": 2 }\n      ]\n    }\n  ]\n}</code></pre>\n<p class=\"prose\">Six handles. All six <code>active</code>. Exactly one also <code>editable</code>, and that one is <code>durov</code>.</p>\n<p class=\"prose\">The <code>flags</code> number is the same information in bitfield form: <code>2</code> is the active bit alone, <code>3</code> is active plus editable. You never need to decode it, the booleans are already there — but seeing 3 against 2 tells you which entry is canonical at a glance.</p>\n<p class=\"prose\">So the rule is not \"take the first active handle\". It is \"take the active and editable one\". That is the handle the owner controls and the one that belongs in a link. The other five are aliases: they work, but they are not the name.</p>\n\n<h2>The opposite case, from the same endpoint</h2>\n<pre><code>curl '<b>https://telegram155.p.rapidapi.com/v1/usernames/telegram</b>' \\\n  --header 'x-rapidapi-key: YOUR_KEY' \\\n  --header 'x-rapidapi-host: telegram155.p.rapidapi.com'</code></pre>\n<pre><code>{ \"chats\": [ { \"id\": 1005640892, \"username\": \"telegram\", \"usernames\": null } ] }</code></pre>\n<p class=\"prose\">One handle, sitting in <code>username</code>, and <code>usernames</code> is <code>null</code>. Both shapes are normal and both arrive from the same route, so code that handles only one of them is wrong half the time.</p>\n\n<h2>Resolving it once, properly</h2>\n<pre><code>def handle(chat):\n    \"\"\"The handle a link should use: active and editable, then any active, then the plain field.\"\"\"\n    names = chat.get(\"usernames\") or []\n\n    for u in names:\n        if u.get(\"active\") and u.get(\"editable\"):\n            return u[\"username\"]\n\n    for u in names:\n        if u.get(\"active\"):\n            return u[\"username\"]\n\n    return chat.get(\"username\") or None</code></pre>\n<p class=\"prose\">Three things worth noticing in nine lines. <code>or []</code> on the array, because <code>usernames</code> is <code>null</code> rather than empty for single-handle channels. The two loops in that order, because \"editable\" is the distinguishing bit and \"active\" alone is not. And <code>None</code> at the end rather than the numeric id, because a link built from an id is worse than no link: it looks valid, it renders, and it goes nowhere.</p>\n<p class=\"prose\">If you do want a fallback that always produces something, use the id for your own bookkeeping and keep it out of anything a person will click.</p>\n\n<h2>Where this bites in practice</h2>\n<p class=\"prose\">Anywhere you turn stored data back into a link: a digest email listing channels, a dashboard row, a CSV export someone opens next month. The case that produces it is a channel that renamed itself and kept the old handles working — exactly when <code>username</code> empties out and nobody notices until the links ship.</p>\n<p class=\"prose\">The check is cheap: for every chat you store, assert that the handle you saved is non-empty and is not the id in string form. One line in a test, and a class of dead links never reaches anyone.</p>\n\n<p class=\"prose\">Resolving a handle is a single request, and the response carries whichever shape that channel has. The free plan is 2,500 calls a month with no card, so mapping a few hundred channels and getting this right costs nothing. <a href=\"https://tgatlas.org/docs\">Full endpoint reference</a>: 19 routes, one API key, no phone number.</p>",
      "date_published": "2026-09-16T00:00:00.000Z"
    },
    {
      "id": "https://tgatlas.org/blog/where-the-subscribers-actually-are",
      "url": "https://tgatlas.org/blog/where-the-subscribers-actually-are",
      "title": "52 channels hold 38 per cent of the audience",
      "summary": "The largest one per cent of the index is 52 channels, and they hold 38.5% of every subscription we measure. The median channel has 15,988.",
      "content_html": "\n<p class=\"prose\">We publish a directory of public Telegram channels and re-measure it every week. This week's export covers 5,205 channels with 642,717,136 subscriptions between them, measured on 14 Sept 2026.</p>\n<p class=\"prose\">Here is what the distribution looks like, because the shape is not what most people assume and nobody seems to publish it.</p>\n\n<h2>The top is very small and very heavy</h2>\n<p class=\"prose\">The largest one per cent of the index is 52 channels. They hold <b>38.5%</b> of all the subscriptions we measure.</p>\n<p class=\"prose\">Widen it to the top ten per cent, 520 channels, and the share is 79.4%. Most of the audience sits in a tenth of the list.</p>\n<p class=\"prose\">The median channel, meanwhile, has <b>15,988</b> subscribers. The mean is 123,481, 7.7 times the median. When a mean runs that far ahead of a median, every average you read about \"Telegram channels\" is describing a handful of giants and nothing else.</p>\n\n<h2>The tail is most of the list</h2>\n<p class=\"prose\"><b>37.9%</b> of the channels we index have fewer than 10,000 subscribers. That is 1,974 channels, and they are the ones a media buyer never sees on a leaderboard.</p>\n<p class=\"prose\">Only 128 channels, 2.5%, have passed a million. Verification is rarer than the badge count suggests too: 567 channels, 10.9%, carry Telegram's verified flag.</p>\n\n<h2>The posting frequency nobody budgets for</h2>\n<p class=\"prose\">The median channel posts 1.89 times a day, across 5,024 channels with a measured history. That is the number to size a reader or a digest around.</p>\n<p class=\"prose\">But <b>148 channels post at least 20 times a day</b>, 2.8% of the index, and that is the cohort that quietly breaks a naive pipeline: a poller sized for two posts a day falls a day behind in its first week and stays there. Those rows carry <code>posts_per_day_is_lower_bound=1</code> in the export, because they filled the whole history window inside a single day and their real rate is higher than the number.</p>\n\n<h2>Recommendations are sparser than the network diagrams imply</h2>\n<p class=\"prose\">Telegram will tell you which channels it considers similar to a given one, and we store that graph. The median channel is recommended by exactly 1 other channel in our index. The most recommended one is pointed at by 47.</p>\n<p class=\"prose\">Only 306 channels, 5.9%, are recommended by nobody at all. So the graph is connected almost everywhere and thin almost everywhere, which is awkward if you were planning to walk it for discovery: you run out of edges before you run out of interest.</p>\n\n<h2>What the topics look like</h2>\n<p class=\"prose\">20 topics cover the index. The five largest: <code>regional-ru</code> with 609 channels, <code>education</code> 548, <code>science</code> 386, <code>news</code> 355, <code>crypto</code> 310.</p>\n<p class=\"prose\">That ranking says more about what gets indexed and recommended than about what exists on Telegram, and we would rather say so than let you read it as a map of the platform.</p>\n\n<h2>How this was measured</h2>\n<p class=\"prose\">One export, <a href=\"https://tgatlas.org/data\">/data/channels.csv</a>, week <code>2026-W38</code>, measured 14 Sept 2026: 5,205 rows, one per channel, keyed by username. The file is public and every number on this page recomputes from it.</p>\n<p class=\"prose\">The cohorts round down. Sort by <code>subscribers</code> descending and take the first <code>floor(0.01 &times; 5,205)</code> = 52 rows for the top one per cent. That cohort is 1.00% of the index, never wider than the share it claims. The same rule gives 520 rows for the top ten per cent. Round up instead and you get one extra channel in each, which is where a recomputed figure usually parts company with a published one.</p>\n<p class=\"prose\">The set is the same one the rest of this site is built from: public channels of 1,000 subscribers and up, minus anything Telegram flags as scam or fake. One filter feeds the pages, the export and this page, so the three never disagree.</p>\n<p class=\"prose\">Growth is published with the interval it was measured over. The two readings behind it are 31 Aug 2026 and 14 Sept 2026, with no reading in between, so the interval is 14 days rather than seven. The column keeps the name <code>weekly_growth_pct</code>, because a column name is a contract with code that already reads it, and <code>growth_days</code> sits beside it in the export so the interval travels with the number. Read the interval, not the column name.</p>\n<p class=\"prose\">The data behind this page is on the <a href=\"https://tgatlas.org/data\">downloads page</a>, and <a href=\"https://tgatlas.org/method\">how it is measured</a> in full. Reading the same fields live, per channel, is one call: the free plan is 2,500 calls a month with no card, and the <a href=\"https://tgatlas.org/docs\">endpoint reference</a> is the whole contract.</p>",
      "date_published": "2026-09-16T00:00:00.000Z"
    },
    {
      "id": "https://tgatlas.org/reports/biggest-channels",
      "url": "https://tgatlas.org/reports/biggest-channels",
      "title": "The biggest public Telegram channels",
      "summary": "Ranked by subscribers, across every topic in the index.",
      "date_published": "2026-09-14T00:00:00.000Z"
    },
    {
      "id": "https://tgatlas.org/reports/fastest-growing",
      "url": "https://tgatlas.org/reports/fastest-growing",
      "title": "Fastest-growing Telegram channels",
      "summary": "Who added the most, proportionally, since the previous measurement.",
      "date_published": "2026-09-14T00:00:00.000Z"
    },
    {
      "id": "https://tgatlas.org/reports/shrinking",
      "url": "https://tgatlas.org/reports/shrinking",
      "title": "Channels losing subscribers",
      "summary": "The half of the index nobody publishes.",
      "date_published": "2026-09-14T00:00:00.000Z"
    },
    {
      "id": "https://tgatlas.org/reports/most-recommended",
      "url": "https://tgatlas.org/reports/most-recommended",
      "title": "The channels Telegram recommends most",
      "summary": "Counted across the whole index, in the direction Telegram does not publish.",
      "date_published": "2026-09-14T00:00:00.000Z"
    },
    {
      "id": "https://tgatlas.org/reports/posting-cadence",
      "url": "https://tgatlas.org/reports/posting-cadence",
      "title": "How often Telegram channels post",
      "summary": "Posts per day, by topic and by size.",
      "date_published": "2026-09-14T00:00:00.000Z"
    },
    {
      "id": "https://tgatlas.org/blog/telegram-has-no-directory",
      "url": "https://tgatlas.org/blog/telegram-has-no-directory",
      "title": "Telegram has no directory. We built one out of its own recommendations",
      "summary": "Search returns ten results and stops. The recommendation graph returns 5,205 channels and keeps going.",
      "content_html": "\n<p class=\"prose\">Telegram has around a billion users and no public directory. There is no category page, no equivalent of a subreddit list. The in-app search box returns roughly ten chats and that is the whole front door.</p>\n<p class=\"prose\">Which means channels are found by word of mouth and forwarded links — and that anyone trying to size a topic is guessing.</p>\n\n<h2>The edge Telegram does publish</h2>\n<p class=\"prose\">For any public channel, Telegram maintains a list of channels it recommends alongside. Ten per lookup. It is a similarity graph, maintained by the platform, and it is the only structured discovery mechanism Telegram offers.</p>\n<p class=\"prose\">Walking it changes the arithmetic completely. A few hundred keyword searches gets a few hundred channels. Expanding along recommendations twice takes that into the thousands, and the graph-found channels are better targeted than the search results, because they are what Telegram itself considers adjacent.</p>\n\n<h2>The direction that is missing</h2>\n<p class=\"prose\">The graph is published in one direction only. You can ask what Telegram shows beside a channel. You cannot ask who has that channel in their list.</p>\n<p class=\"prose\">Inverting it across the whole index is a few lines of code and produces something Telegram does not have: inbound recommendation degree. Outbound edges say what a channel is like; inbound degree says where it sits.</p>\n<p class=\"prose\">The two rankings disagree, and interestingly so. Very large channels often have low inbound degree, because Telegram places them beside other giants and giants are scarce. Mid-sized channels in dense niches accumulate far more. If the question is which channel anchors a topic, inbound degree answers it better than subscriber count does.</p>\n\n<p class=\"prose\">The current index holds 5,205 public channels across 20 topics, 642,717,136 subscribers between them. <a href=\"https://tgatlas.org/method\">How it is measured</a>, and <a href=\"https://tgatlas.org/reports/most-recommended\">who Telegram recommends most</a>.</p>",
      "date_published": "2026-09-05T00:00:00.000Z"
    },
    {
      "id": "https://tgatlas.org/blog/subscriber-counts-have-no-history",
      "url": "https://tgatlas.org/blog/subscriber-counts-have-no-history",
      "title": "Telegram shows you today's subscriber count and never yesterday's",
      "summary": "Median growth across the index is -0.14% over 14 days, and 60.6% of channels are shrinking. Neither number exists inside Telegram.",
      "content_html": "\n<p class=\"prose\">Open any public Telegram channel and you see a subscriber count. Open it tomorrow and you see a different one. What you never see is the first number again.</p>\n<p class=\"prose\">Telegram keeps no public history, which means every claim about channel growth is either someone's private archive or a guess. Usually a guess.</p>\n\n<h2>What a series actually shows</h2>\n<p class=\"prose\">Once you store the readings weekly, three things become visible that a single snapshot hides.</p>\n<p class=\"prose\"><b>Most channels barely move.</b> Median growth across the index is -0.14% over 14 days. The channels people quote as examples of Telegram growth are the tail, not the distribution.</p>\n<p class=\"prose\"><b>A large share shrink.</b> 60.6% of measured channels had fewer subscribers than 14 days earlier. Every directory publishes the winners; decline is the more useful half if you are deciding where to spend.</p>\n<p class=\"prose\"><b>Size and momentum are unrelated.</b> The biggest channels in the index are not the fastest growing, and the fastest growing are mostly small. Ranking by size and ranking by growth produce two nearly disjoint lists.</p>\n\n<h2>The part you cannot backfill</h2>\n<p class=\"prose\">This is the whole argument for starting a measurement before you need it. A competitor who begins today has one reading. In three months they have three months. The gap does not close by spending money.</p>\n<p class=\"prose\">Which is also why a page here publishes only once it has two readings: one is a screenshot anyone can take, two is a series.</p>\n\n<p class=\"prose\"><a href=\"https://tgatlas.org/reports/fastest-growing\">Who is growing</a> and <a href=\"https://tgatlas.org/reports/shrinking\">who is not</a>, both recalculated weekly.</p>",
      "date_published": "2026-09-05T00:00:00.000Z"
    },
    {
      "id": "https://tgatlas.org/blog/mtproto-not-bot-api",
      "url": "https://tgatlas.org/blog/mtproto-not-bot-api",
      "title": "Why a Telegram bot cannot read the channel you want",
      "summary": "The Bot API is a permission boundary, not a missing feature. No amount of work gets you around it.",
      "content_html": "\n<p class=\"prose\">The most common question about reading Telegram programmatically has an answer people do not want: the Bot API cannot do it.</p>\n<p class=\"prose\">A bot only sees chats it belongs to, and for channels it must be an administrator to receive posts. There is no method that takes an arbitrary <code>@username</code> and returns its messages. That is deliberate — a permission boundary, not a gap someone forgot to fill.</p>\n<p class=\"prose\">So if you do not control the channel, the Bot API is the wrong tool, and no amount of engineering makes it the right one.</p>\n\n<h2>What works instead</h2>\n<p class=\"prose\">MTProto is the protocol the Telegram clients themselves speak. A client session resolves any public username and reads the channel exactly as a logged-out visitor would. Telethon and Pyrogram both wrap it, and for a script that reads one channel occasionally that is genuinely the right answer.</p>\n\n<h2>What it costs to run yourself</h2>\n<p class=\"prose\">Three things, worth knowing before you commit.</p>\n<p class=\"prose\"><b>Credentials.</b> API id and hash come from my.telegram.org and require a phone number. The session file that results is state you must persist and protect: losing it means re-authenticating, leaking it is account compromise.</p>\n<p class=\"prose\"><b>Flood control.</b> Telegram returns a wait duration and you must actually wait it out. This is not optional and not negotiable.</p>\n<p class=\"prose\"><b>Concurrency is not yours to choose.</b> The session holds state, so parallelism is limited by the protocol rather than by your machine. We learned this the direct way: a crawl at six concurrent requests took a gateway down and every endpoint returned errors for an hour afterwards.</p>\n\n<p class=\"prose\">That last point generalises beyond Telegram. A REST façade over a stateful protocol does not inherit REST's concurrency properties, however much the URL shape suggests otherwise.</p>",
      "date_published": "2026-09-05T00:00:00.000Z"
    },
    {
      "id": "https://tgatlas.org/blog/what-we-do-not-collect",
      "url": "https://tgatlas.org/blog/what-we-do-not-collect",
      "title": "What this index deliberately does not collect",
      "summary": "Public channel metadata and post timestamps. Not message contents, not member lists, nothing from private groups.",
      "content_html": "\n<p class=\"prose\">Any project that reads a messaging platform at scale has to answer where it stops. Ours stops in a specific place, and the reasoning is worth writing down rather than leaving implied.</p>\n\n<h2>What is collected</h2>\n<p class=\"prose\">What Telegram shows any logged-out visitor to a public channel: name, <code>@username</code>, description, subscriber count, creation date, verification flags, and the recommendation list. Plus the timestamps of recent posts, from which a posting rate is computed.</p>\n\n<h2>What is not</h2>\n<p class=\"prose\"><b>Message contents.</b> Timestamps give the cadence; the text is somebody else's writing and republishing a collection of it is not ours to do. It is also, incidentally, a much larger disk bill for information we would not use.</p>\n<p class=\"prose\"><b>Member lists.</b> Even where an endpoint exposes participants, enumerating the members of a channel is a different act from measuring the channel.</p>\n<p class=\"prose\"><b>Anything private.</b> Only channels with a public username. A group that requires an invite is out of scope, and there is no configuration flag that changes that.</p>\n\n<h2>Why the line sits there</h2>\n<p class=\"prose\">Partly principle and partly durability. The adjacent lesson in this industry is Proxycurl, shut down in 2025 — not for collecting too much, but for where the data came from. Provenance decides whether a project survives, and a line drawn at \"what a logged-out visitor sees\" is one you can state in a sentence and defend without a lawyer.</p>\n<p class=\"prose\">If you run a channel indexed here and want it removed, that request is honoured. The address is in the <a href=\"https://tgatlas.org/terms\">terms</a>.</p>",
      "date_published": "2026-09-05T00:00:00.000Z"
    },
    {
      "id": "https://tgatlas.org/blog/size-is-not-standing",
      "url": "https://tgatlas.org/blog/size-is-not-standing",
      "title": "The biggest channels are not the most recommended ones",
      "summary": "Ranking 5,205 channels by subscribers and by inbound recommendations produces two almost disjoint lists.",
      "content_html": "\n<p class=\"prose\">There are two ways to ask which Telegram channel matters most in a topic. They give different answers, and the less obvious one is more useful.</p>\n\n<h2>Subscribers measure accumulated audience</h2>\n<p class=\"prose\">It is the number everyone quotes because it is the number Telegram shows. It rewards age, virality and whatever the channel did two years ago, and it is nearly impossible to lose.</p>\n\n<h2>Inbound recommendations measure current standing</h2>\n<p class=\"prose\">Telegram places channels beside one another. Count how often a channel appears in other channels' lists and you get a measure of where it sits in its neighbourhood right now — independent of how large it grew.</p>\n<p class=\"prose\">Across the index those two rankings barely overlap. The largest channels frequently have low inbound degree, because Telegram places giants beside other giants and there are not many. Mid-sized channels in dense niches accumulate far more edges.</p>\n\n<h2>Which to use</h2>\n<p class=\"prose\">If you are buying reach, subscribers. If you are trying to find the channel that anchors a subject — the one whose neighbours are all the other channels you care about — inbound degree, every time.</p>\n<p class=\"prose\">Telegram publishes the outbound direction and not the inbound one, so this measure only exists if somebody crawls the graph and turns it around. <a href=\"https://tgatlas.org/reports/most-recommended\">The current ranking</a>, recalculated weekly.</p>",
      "date_published": "2026-09-05T00:00:00.000Z"
    },
    {
      "id": "https://tgatlas.org/blog/health-checks-that-lie",
      "url": "https://tgatlas.org/blog/health-checks-that-lie",
      "title": "A health check that returns 200 while everything else fails",
      "summary": "The liveness probe answered fine for an hour while every real endpoint returned 503. That is worse than having no probe.",
      "content_html": "\n<p class=\"prose\">Building this index we ran a crawl at six concurrent requests against a Telegram gateway. It took the service down in about six minutes — every real endpoint returning 503 for the next hour.</p>\n<p class=\"prose\">Throughout, <code>/health</code> returned <code>200 {\"status\":\"ok\"}</code>.</p>\n\n<h2>Why that is worse than nothing</h2>\n<p class=\"prose\">A probe that does not touch the thing the service exists for is not a weaker signal. It is a false one. Teams point monitoring at it, the dashboard stays green, and the outage is discovered by a customer instead.</p>\n<p class=\"prose\">Had there been no <code>/health</code> at all, the monitoring would have been built against a real endpoint and would have fired.</p>\n\n<h2>What a useful probe looks like</h2>\n<p class=\"prose\">It exercises the dependency that actually fails. For a service wrapping a stateful protocol, that means touching the session — a cheap fixed lookup with a short cache is enough. It costs one upstream call a minute and tells you the truth.</p>\n<p class=\"prose\">The tell is in the failure message. Ours suggested calling a specific endpoint \"first\", which means the service has warm-up state. Any probe that does not exercise that state is measuring the HTTP server, not the product.</p>\n\n<h2>The general rule</h2>\n<p class=\"prose\">Ask what your health check would have caught in your last outage. If the answer is nothing, it is decoration.</p>\n<p class=\"prose\">The same reasoning shapes this site: the build refuses to publish if the measurement is stale or the aggregate moves implausibly, because a pipeline that fails quietly and keeps publishing is the same failure in a different costume.</p>",
      "date_published": "2026-09-05T00:00:00.000Z"
    }
  ]
}