Bot API vs MTProto
Telegram exposes two surfaces — the HTTP Bot API and the binary MTProto protocol. teleproto speaks MTProto. Here's the honest comparison.
The two surfaces#
Bot API. HTTPS, JSON request/response, bot accounts only. Each call is a fresh HTTP round-trip. Receiving updates means either standing up an inbound webhook (public URL, TLS cert) or long-polling getUpdates on a loop.
MTProto. Binary frames over a persistent TCP connection. User or bot accounts. Updates push to you over the same socket you use to send requests — no inbound endpoint, no public URL, no certificate plumbing. Every method in the TL schema is callable, including ones the Bot API never exposed.
When Bot API wins#
- You only need to send notifications or alerts. One-way outbound traffic from your server to a channel or chat. Reach for grammY — you'll be done in five lines.
- Stateless serverless with no persistent connection. Cloud Functions, Lambda, Cloudflare Workers. Bot API is HTTP and survives a cold start; MTProto needs a live socket and a session that outlives any single invocation.
- You don't care about per-user actions. Your bot's job is to talk; it never needs to read history, join private channels, or impersonate a human account. The Bot API is the smaller surface, and smaller is better when smaller is enough.
When MTProto wins#
- You need a user account. History, dialogs, joining channels, profile edits, contacts, account-level operations. The Bot API simply can't do these.
- Userbots — automation as your own user. Auto-react, schedule, monitor, archive. Same surface the official Telegram client uses.
- Push updates without exposing a webhook. Updates arrive on the existing MTProto socket. No public URL, no TLS cert, no NAT punching. Works fine behind a residential firewall.
- TL methods the Bot API never exposes. Advanced channel ops, takeout sessions, full peer info, secret chat machinery, layered protocol features.
- Lower overhead under load. Binary frames on one socket beats one HTTPS round-trip per call once throughput matters. Replies feel snappier.
Quick comparison#
The condensed version of everything above:
| Feature | Bot API | MTProto (teleproto) |
|---|---|---|
| Transport | HTTPS / JSON | TCP / binary TL |
| Accounts | Bot only | User or bot |
| Inbound endpoint | Webhook URL or long-poll | None — updates push on socket |
| Per-method schema | Curated subset (~100 methods) | Full TL schema (~1000 methods) |
| User actions | Not supported | First-class |
| File size limit | 20 MB down / 50 MB up | Up to 2 GB / 4 GB Premium |
| Implementation effort | Minimal | Higher — sessions, entity cache, reconnects |
| Best for | Notifications, simple bots | Userbots, automation, advanced bots, scale |
Hybrid setups#
The two surfaces aren't mutually exclusive. Plenty of production deployments run both: a bot account on the Bot API for the public-facing /commands and inline queries, plus a userbot on teleproto doing the heavy lifting — scraping history, watching dozens of channels, batch-downloading media. The two processes coordinate through Telegram itself, usually by having one write to a private channel that the other listens on.
// Bot account (Bot API) posts a public command response.
import { Bot } from "grammy";
const bot = new Bot(process.env.BOT_TOKEN!);
bot.command("digest", async (ctx) => {
await ctx.reply("Generating digest...");
// Tell the userbot to do the heavy lifting via a private channel.
await ctx.api.sendMessage(process.env.COORD_CHANNEL!, `digest:${ctx.chat.id}`);
});
bot.start();
// Userbot (teleproto) reads the coordination channel and does the work.
import { TelegramClient, Api } from "teleproto";
import { StoreSession } from "teleproto/sessions";
import { NewMessage } from "teleproto/events";
const client = new TelegramClient(
new StoreSession("userbot"),
Number(process.env.TG_API_ID),
process.env.TG_API_HASH!,
{ connectionRetries: 5 },
);
await client.start({ /* ...auth params... */ });
client.addEventHandler(async (event) => {
const text = event.message.message ?? "";
if (!text.startsWith("digest:")) return;
const chatId = text.slice("digest:".length);
const history = await client.getMessages("@somechannel", { limit: 100 });
// ...summarise, then push back through the Bot API...
}, new NewMessage({ chats: [Number(process.env.COORD_CHANNEL_ID)] }));teleproto is for everything beyond Bot API#
If your whole need is "send my server's alerts to a Telegram channel," reach for grammY and move on. teleproto exists for the rest — userbots, full account access, the raw TL surface, and anything MTProto can do that HTTP can't.