Skip to content
teleproto

Connection lifecycle

Everything in teleproto rides on one persistent TCP connection per data center. This page is what happens under client.connect() — the DC topology, the framing on the wire, and the state machine that keeps the socket alive so your handlers never notice a blip.

Telegram's data centers#

Telegram shards accounts across five logical data centers, numbered DC 1–5. Your account has a home DC — assigned at signup, encoded into your session — and every API call you make is answered there. Files are different: media can live on anyDC (and on CDN DCs for popular public content), which is why a download sometimes needs a connection your client doesn't have yet.

teleproto addresses those extra connections with shifted DC idsinternally — the same DC number, offset to mark it as a media or CDN lane — so a media connection to DC 4 never collides with the main API connection to DC 4 in the connection table. Each DC also publishes several IP addresses; teleproto keeps the list from Telegram's config and rotates through it when an address stops answering.

Wrong-DC answers are part of the protocol, not an error you handle: UserMigrateError and friends carry .newDc and teleproto re-dials transparently — see Errors & FloodWait.

One lifecycle, four states#

Since v1.228 every connection — main API, media, CDN — runs the same explicit state machine instead of ad-hoc reconnect logic sprinkled through the stack:

state       -> next        on
--------------------------------------------------------------------
idle        -> connecting  connect(): dial the DC address
connecting  -> ready       TCP up, handshake done, InitConnection sent
connecting  -> connecting  attempt timed out: next / alternate address
connecting  -> failed      connectionRetries exhausted
ready       -> broken      socket error, missed pong, server close
broken      -> connecting  autoReconnect (default) after retryDelay
  • Connecting has a real deadline. A dial that hangs (dead IP, black-holed route) times out and moves to the next address for that DC instead of blocking forever.
  • Everything owned by a connection dies with it. Temporary auth keys, in-flight request state, send queues — all scoped to the connection object. A rebuilt connection starts clean, which is what fixed the class of "stale temp key" deadlocks older versions could hit after a drop.
  • Failed requests fail fast.Requests in flight when the socket breaks are retried on the new connection or surfaced as errors — they don't hang until a human restarts the process.

Transport & framing#

MTProto is not HTTP. The wire format is length-prefixed binary frames over a raw TCP socket (or tunneled through SOCKS5 / MTProxy — including ee fake-TLS secrets, where the whole stream hides inside a TLS 1.2-shaped handshake). teleproto reads with exact-read framing: it learns the frame length from the header and reads precisely that many bytes, buffering the socket in chunks instead of copying per read. Combined with single-copy encryption on the send path, a file part traverses memory roughly once instead of four times — most of the v1.228 throughput win.

The socket runs with TCP keepalive on, and MTProto's own ping/pong (ping_delay_disconnect) rides on top: the server drops the connection if pings stop arriving, and the client treats a missed pong as a broken socket. Ping timeouts are lifecycle events — since v1.227.4 they no longer kill transfers that are demonstrably still moving bytes.

What connect() actually does#

In order, on a fresh session:

  1. Dial the DC address from the session (or the default DC).
  2. Negotiate an auth key if the session doesn't have one for this DC — the Diffie-Hellman exchange described in Auth keys & encryption. With a saved session this step is skipped entirely; that's the whole point of persisting sessions.
  3. Send InvokeWithLayer(InitConnection(...)) wrapping the first real request — this pins the TL layer and registers the device fingerprint for the connection. Every fresh DC sender does this locally, so parallel connections to different DCs can't race each other's init state.
  4. Start the read loop, the ping loop, and hand the connection to the sender pool.

Reconnects & failover#

Drops are normal — Telegram rebalances, NATs expire, Wi-Fi roams. With autoReconnect: true (the default) the lifecycle loops through broken, connecting, ready on its own, re-sending what was in flight. Transport-level rejections (the 4-byte 404/429/444 answers Telegram uses below the MTProto layer) are mapped to real errors and recovery paths instead of hanging the read loop; a server-side auth-key loss (transport 404) triggers a re-handshake automatically, including for media temp keys.

tuning.ts
const client = new TelegramClient(session, apiId, apiHash, {
  connectionRetries: 5, // attempts per connect() before giving up
  retryDelay: 1000,     // ms between attempts
  autoReconnect: true,  // default — rebuild the connection on drops
  useIPV6: false,       // opt in on v6-only networks
});

Observing connection state#

Connection state changes flow through the normal update pipeline, so monitoring is just another event handler:

observe.ts
import { Raw } from "teleproto/events";

// Connection state changes are dispatched to event handlers
// like any other update — no separate listener API to learn.
client.addEventHandler((update) => {
  switch (update.className) {
    case "UpdateConnectionState":
      console.log("connection state:", update.state); // connected / disconnected / broken
      break;
  }
}, new Raw({}));

In production you rarely need to act on these — the lifecycle recovers on its own — but exporting them as metrics gives you the reconnect-rate graph that tells you when a host or proxy is degrading. What you should handle is covered in Production.