Update pipeline
Updates & Eventscovers the consumer side — builders, filters, handlers. This page is the machinery underneath: how teleproto knows an update went missing, how it backfills, and why you don't see duplicates.
Push, not polling#
There is no getUpdateslong-poll here. The server writes update containers into the same TCP connection your requests ride on, whenever it wants. That's the whole reason userbots need no webhook and no public endpoint — but it also means delivery is only as reliable as one TCP stream, and TCP streams break. Everything else on this page exists to reconcile "what the server did" with "what this client saw" after any interruption.
pts, qts, seq#
Telegram versions its state with monotonic counters, and every update says which counter positions it represents:
counter scope moves on
---------------------------------------------------------------------
pts account-wide common state new / edited / deleted messages,
(DMs, small groups) read markers, ...
pts each channel / supergroup the same events, per channel
(independent counter each)
qts secret-chat & bot events encrypted events, bot callbacks
seq the update container stream container-level bookkeepingThe important asymmetry: channels have their own pts each. Your common state can be perfectly in sync while one busy supergroup is behind, and vice versa — which is why backfill has two RPCs, not one. teleproto persists the counters (plus the last-seen date) in the session on every applied update; that persisted tuple is what makes restart recovery possible at all.
Gap detection & getDifference#
incoming update carries (pts, pts_count)
expected: local_pts + pts_count == update.pts
match: apply, set local_pts = update.pts
too high: a gap -- buffer briefly, then
updates.getDifference(local_pts, ...) for common state
channels.getDifference(channel, pts) for that channel only
too low: already applied -- drop (dedup)A gap means the socket dropped an update on the floor — rebalancing, a reconnect, server-side shedding. teleproto buffers the out-of-order update briefly (the missing one often arrives a moment later), then gives up and asks for the delta: updates.getDifference returns every missed message, edit, and read-marker since your pts, and the pipeline feeds them to your handlers like live events.
If the client is too far behind, the server refuses to enumerate and answers differenceTooLong with a fresh pts — the events in between are unrecoverable through the update system, and anything you need from that window has to be refetched via iterMessages. The same signal arrives live as UpdatesTooLong.
catchUp() after a restart#
await client.connect();
await client.catchUp(); // replay what happened while the process was down
// The session persisted pts/qts/date at last shutdown;
// catchUp() runs getDifference from that state until Telegram
// answers differenceEmpty. Handlers fire for each replayed event
// exactly as if it had arrived live.Without catchUp(), connecting only subscribes you to newupdates; downtime is a blind spot. With it, the persisted counters bound the blind spot to whatever Telegram is willing to replay (typically ample for restarts and deploys, not for a week offline). It's deliberately opt-in: a bot that only reacts to fresh commands usually wants to drop the backlog rather than answer three-hour-old messages.
Deduplication#
The pts comparison ("too low: drop") is the dedup mechanism, and it has one historical hole: some events are dispatched with pts: 0 — outside the counter system. A message delivered both live and in a getDifference replay could reach handlers twice through that hole. v1.225.4 closed it by tracking recent message identities across both paths; a NewMessage handler now fires once per message, full stop. If you keep your own idempotency keys anyway (you should, for at-least-once job queues), key on chatId + msgId, not on handler invocations.
Ordering guarantees#
Within one chat, updates apply in pts order — you will not see an edit before its message. Across chats there is no global order, and handler executionis concurrent by default: an async handler awaiting a slow call doesn't block the next event. If your logic needs strict serialization, opt in with sequentialUpdates: true on the client params, and read the trade-offs in Updates & Events — one slow handler then stalls the whole stream.