Telegram userbots — MTProto, and what it costs
A bot token is issued to software. An MTProto session is a logged-in human
being — the same authority the person has, held in a file, with none of the
protections a token has. Everything in this skill follows from that one
difference.
Read against Telethon 1.44.0 and core.telegram.org on 2026-08-25. Telethon
minor releases change session and entity-cache behaviour; the version you are on
is a fact to check, not to assume.
Deep material, loaded on demand:
| Read |
When |
references/sessions-and-auth.md |
logging in, storing a session, rotating one, or moving between machines |
references/rate-and-flood.md |
anything that loops over chats, users or messages — FloodWait, pacing, takeout |
references/entities-and-history.md |
resolving peers, iterating history, downloading media at scale |
First: do you actually need one?
Answer this before writing a line, and write the answer down.
| The job |
Bot API can |
Verdict |
| React to messages in chats the bot is in |
yes |
use a bot |
| Send to users who started the bot |
yes |
use a bot |
| Read a public channel it is admin of |
yes |
use a bot |
| Read history of a channel it does not administer |
no |
userbot |
| Read a chat's past, from before it joined |
no |
userbot |
| Act as a specific person |
no |
userbot |
| Download a file over 20 MB |
no |
userbot, or a local Bot API server |
| Enumerate a group's members at scale |
no |
userbot, and see the risk below |
The local Bot API server closes the file ceiling without an account. Reaching
for a user session because of a 21 MB video is trading a permanent liability for
an infrastructure task you were going to have anyway.
The refusal, and it is not a formality: a user account can be limited or
banned, and Telegram does not explain, appeal quickly, or restore what was in it.
If the account is a real person's, the blast radius is their messages, their
groups and their logins. Automating a personal account is a decision with a
named owner or it is not a decision.
The session file is the credential
from telethon import TelegramClient
client = TelegramClient(
session=StringSession(os.environ["TG_SESSION"]), # from the secret store
api_id=int(os.environ["TG_API_ID"]),
api_hash=os.environ["TG_API_HASH"],
)
- A
.session file grants full access to the account, without the password and
without the 2FA prompt. Copying one to another machine logs that machine in.
It belongs in .gitignore, in a secret store, in backups you would give a
password the same treatment — and never in an image layer or a log.
api_id/api_hash come from my.telegram.org and identify the
application, not the account. They are not a session and not a secret of the
same weight, but they are still not public.
- Losing a session is recoverable; leaking one is an incident. Revoke from
the account's own Devices screen, which ends that session everywhere.
- Verified on this machine on 2026-08-25: five of five Telethon projects
gitignore the session and none has one tracked in git — the one trap this
estate has actually closed. Keep it closed.
FloodWaitError is the API working
from telethon.errors import FloodWaitError
try:
await client.send_message(peer, text)
except FloodWaitError as e:
if e.seconds > MAX_ACCEPTABLE: # a decision, not a sleep
raise
await asyncio.sleep(e.seconds + 1)
await client.send_message(peer, text)
- It is not a failure to swallow and not a signal to retry blindly. It names
exactly how long to wait; anything else is guessing at a number Telegram
already told you.
- A very large
seconds is a different event. Minutes mean you are pacing
too hard; hours mean the account is being limited, and continuing is how a
limit becomes a ban. Cap it, alert, stop.
- Sleeping inside a request handler is how one flood becomes an outage. The
wait belongs in a worker with a queue, not in the path a user is waiting on.
- Measured across six Telethon projects here on 2026-08-25: three of six handle
FloodWaitError at all. The other three run until the first limit and then
stop, in whatever state they were in.
Pin the minor, and know why
Four bots in this estate pin telethon==1.37.0 while a fifth runs 1.44.0, and
the pin carries its reason in requirements.txt: minor releases change session
and entity-cache behaviour, and a jump of seven minors on a live bot needs its own
change with its own verification.
That is the right shape and it is worth stating as doctrine. A userbot's
dependency is not a library, it is a protocol client holding a login. An
upgrade can invalidate a session format, change what get_entity costs, or move
an exception's module. Upgrade deliberately, one bot at a time, with a session
you can recreate.
Entities are resolved, and resolution is not free
get_entity may hit the network, and doing it in a loop is the most common way
to earn a FloodWait that looks unexplained.
- Prefer ids you already have. A cached
InputPeer costs nothing; a username
costs a request.
PeerIdInvalidError usually means the account has never seen that peer, not
that the id is wrong. A user account can only address what it has encountered.
- Iterate with the library's own iterators (
iter_messages, iter_dialogs)
and let them page; hand-rolled offsets re-request and re-trip the limits.
- For a bulk export,
takeout exists and is the sanctioned path — it raises
the limits for exactly this and tells Telegram what you are doing.
Detail in references/entities-and-history.md.
Two accounts, two lifetimes
A userbot has a second failure mode a bot does not: the human logs in
somewhere, changes the password, or terminates sessions, and your process dies
holding a session that is no longer valid. Treat it as an expected event —
surface it as an alert with the account named, not as a crash loop — and never
put a userbot on the critical path of something a bot could serve.
Before you ship
- The reason a user account is required is written down, and the bot-API
alternative was checked (§ First: do you actually need one?).
- The session is in a secret store, gitignored, and revocable (§ The
session file is the credential).
FloodWaitError is caught, capped and alerted on — never slept off
unbounded (§ FloodWaitError is the API working).
- The client version is pinned, and the upgrade is its own change (§ Pin
the minor).
- A dead session pages a human rather than restarting forever (§ Two
accounts, two lifetimes).
1---2name: telegram-userbots3description: Use when a Telegram job needs a user account rather than a bot token — reading history a bot cannot see, acting as a person, exporting at scale, or downloading past the Bot API ceiling — with Telethon or another MTProto client. Covers the decision of whether a user account is needed at all, the session file as a credential equal to the password, api_id and api_hash, FloodWaitError as the API working rather than failing, entity resolution and its cache, pinning across minor releases that move session and cache behaviour, two-factor login, takeout for bulk export, and the account-ban risk a bot token does not carry. Triggers - "telethon", "pyrogram", "mtproto", "userbot", "user account", "read channel history", "FloodWaitError", "api_hash", "юзербот", "телетон", "мтпрото", "сессия телеграм". Not for ordinary bots (telegram-bots) or the web layer (telegram-miniapps).4license: MIT5---67# Telegram userbots — MTProto, and what it costs89A bot token is issued to software. **An MTProto session is a logged-in human10being** — the same authority the person has, held in a file, with none of the11protections a token has. Everything in this skill follows from that one12difference.1314*Read against Telethon **1.44.0** and `core.telegram.org` on 2026-08-25. Telethon15minor releases change session and entity-cache behaviour; the version you are on16is a fact to check, not to assume.*1718Deep material, loaded on demand:1920| Read | When |21|---|---|22| [`references/sessions-and-auth.md`](references/sessions-and-auth.md) | logging in, storing a session, rotating one, or moving between machines |23| [`references/rate-and-flood.md`](references/rate-and-flood.md) | anything that loops over chats, users or messages — FloodWait, pacing, takeout |24| [`references/entities-and-history.md`](references/entities-and-history.md) | resolving peers, iterating history, downloading media at scale |2526---2728## First: do you actually need one?2930Answer this before writing a line, and write the answer down.3132| The job | Bot API can | Verdict |33|---|---|---|34| React to messages in chats the bot is in | yes | **use a bot** |35| Send to users who started the bot | yes | **use a bot** |36| Read a public channel it is admin of | yes | **use a bot** |37| Read history of a channel it does not administer | no | userbot |38| Read a chat's past, from before it joined | no | userbot |39| Act as a specific person | no | userbot |40| Download a file over 20 MB | no | userbot, **or a local Bot API server** |41| Enumerate a group's members at scale | no | userbot, and see the risk below |4243**The local Bot API server closes the file ceiling without an account.** Reaching44for a user session because of a 21 MB video is trading a permanent liability for45an infrastructure task you were going to have anyway.4647**The refusal, and it is not a formality:** a user account can be limited or48banned, and Telegram does not explain, appeal quickly, or restore what was in it.49If the account is a real person's, the blast radius is their messages, their50groups and their logins. Automating a personal account is a decision with a51named owner or it is not a decision.5253## The session file is the credential5455```python56from telethon import TelegramClient5758client = TelegramClient(59 session=StringSession(os.environ["TG_SESSION"]), # from the secret store60 api_id=int(os.environ["TG_API_ID"]),61 api_hash=os.environ["TG_API_HASH"],62)63```6465- **A `.session` file grants full access to the account, without the password and66 without the 2FA prompt.** Copying one to another machine logs that machine in.67 It belongs in `.gitignore`, in a secret store, in backups you would give a68 password the same treatment — and never in an image layer or a log.69- **`api_id`/`api_hash` come from `my.telegram.org`** and identify the70 *application*, not the account. They are not a session and not a secret of the71 same weight, but they are still not public.72- **Losing a session is recoverable; leaking one is an incident.** Revoke from73 the account's own *Devices* screen, which ends that session everywhere.74- Verified on this machine on 2026-08-25: five of five Telethon projects75 gitignore the session and **none has one tracked in git** — the one trap this76 estate has actually closed. Keep it closed.7778## `FloodWaitError` is the API working7980```python81from telethon.errors import FloodWaitError8283try:84 await client.send_message(peer, text)85except FloodWaitError as e:86 if e.seconds > MAX_ACCEPTABLE: # a decision, not a sleep87 raise88 await asyncio.sleep(e.seconds + 1)89 await client.send_message(peer, text)90```9192- **It is not a failure to swallow and not a signal to retry blindly.** It names93 exactly how long to wait; anything else is guessing at a number Telegram94 already told you.95- **A very large `seconds` is a different event.** Minutes mean you are pacing96 too hard; hours mean the account is being limited, and continuing is how a97 limit becomes a ban. Cap it, alert, stop.98- **Sleeping inside a request handler is how one flood becomes an outage.** The99 wait belongs in a worker with a queue, not in the path a user is waiting on.100- Measured across six Telethon projects here on 2026-08-25: **three of six handle101 `FloodWaitError` at all.** The other three run until the first limit and then102 stop, in whatever state they were in.103104## Pin the minor, and know why105106Four bots in this estate pin `telethon==1.37.0` while a fifth runs `1.44.0`, and107the pin carries its reason in `requirements.txt`: *minor releases change session108and entity-cache behaviour, and a jump of seven minors on a live bot needs its own109change with its own verification.*110111That is the right shape and it is worth stating as doctrine. **A userbot's112dependency is not a library, it is a protocol client holding a login.** An113upgrade can invalidate a session format, change what `get_entity` costs, or move114an exception's module. Upgrade deliberately, one bot at a time, with a session115you can recreate.116117## Entities are resolved, and resolution is not free118119`get_entity` may hit the network, and doing it in a loop is the most common way120to earn a FloodWait that looks unexplained.121122- **Prefer ids you already have.** A cached `InputPeer` costs nothing; a username123 costs a request.124- **`PeerIdInvalidError` usually means the account has never seen that peer**, not125 that the id is wrong. A user account can only address what it has encountered.126- **Iterate with the library's own iterators** (`iter_messages`, `iter_dialogs`)127 and let them page; hand-rolled offsets re-request and re-trip the limits.128- For a bulk export, **`takeout` exists and is the sanctioned path** — it raises129 the limits for exactly this and tells Telegram what you are doing.130131Detail in [`references/entities-and-history.md`](references/entities-and-history.md).132133## Two accounts, two lifetimes134135A userbot has a second failure mode a bot does not: **the human logs in136somewhere, changes the password, or terminates sessions**, and your process dies137holding a session that is no longer valid. Treat it as an expected event —138surface it as an alert with the account named, not as a crash loop — and never139put a userbot on the critical path of something a bot could serve.140141## Before you ship1421431. **The reason a user account is required is written down**, and the bot-API144 alternative was checked (§ *First: do you actually need one?*).1452. **The session is in a secret store, gitignored, and revocable** (§ *The146 session file is the credential*).1473. **`FloodWaitError` is caught, capped and alerted on** — never slept off148 unbounded (§ *`FloodWaitError` is the API working*).1494. **The client version is pinned**, and the upgrade is its own change (§ *Pin150 the minor*).1515. **A dead session pages a human** rather than restarting forever (§ *Two152 accounts, two lifetimes*).