Skip to content
teleproto

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:

FeatureBot APIMTProto (teleproto)
TransportHTTPS / JSONTCP / binary TL
AccountsBot onlyUser or bot
Inbound endpointWebhook URL or long-pollNone — updates push on socket
Per-method schemaCurated subset (~100 methods)Full TL schema (~1000 methods)
User actionsNot supportedFirst-class
File size limit20 MB down / 50 MB upUp to 2 GB / 4 GB Premium
Implementation effortMinimalHigher — sessions, entity cache, reconnects
Best forNotifications, simple botsUserbots, 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.

hybrid.ts
// 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.