← All posts
Guide

Telegram getUpdates Offset: Prevent Duplicate and Lost Updates

Telegram confirms an update when you call getUpdates with an offset greater than that update’s update_id. Merely receiving the response is not the acknowledgement. To avoid losing work, durably store the returned updates before issuing the next request with a higher offset. To handle restarts safely, persist the next offset with that storage and deduplicate updates separately from business processing.

Key takeaways

  • offset is an acknowledgement boundary, not a page number or message count.
  • Advance only after every update you are confirming is safely stored.
  • Keep one active polling owner per bot; scale downstream workers instead.
  • A durable inbox protects intake, but external side effects still need their own retry strategy.

This guide assumes you have already chosen polling. If a webhook is still configured, use the getUpdates and setWebhook switching runbook first. The question here is what to commit between successful polling requests—not which receiving mode to choose.

What the getUpdates offset actually confirms

The official Telegram Bot API reference defines offset as the identifier of the first update to return. Without it, Telegram returns updates starting with the earliest unconfirmed update. A subsequent call with an offset above an update’s identifier confirms that update.

Hypothetical example: a response contains updates 8100, 8101, and 8102. Calling again with offset=8103 confirms all three, even if your application has only processed the last one. Telegram does not inspect your database or wait for your CRM to finish.

This makes two shortcuts unsafe:

ShortcutFailure modeSafer design
Save the highest received ID before storing the batchA restart can resume beyond updates that were never savedCommit the inbox and next offset together
Advance when the fastest parallel task finishesEarlier unfinished updates can be confirmed tooBase intake progress on durable storage, not worker completion order
Keep the offset only in memoryA restart loses the local checkpointReload a durable per-bot checkpoint
Use a negative offset to “fix” duplicatesEarlier queued updates are discardedDiagnose checkpoint ownership and deduplication

Telegram explicitly says a negative offset reads from the end of the update queue and forgets earlier updates. It is not a recovery mechanism for production work you still need.

Keep intake progress separate from business completion

A practical design uses two application-owned records: an inbox containing the full returned update, and a checkpoint containing the next offset. These are local storage concepts, not additional Telegram API fields.

Use the bot’s stable identity together with update_id as the inbox uniqueness key. Do not use the bot token itself as a database key or put it in logs. Store all returned update types, including ones your current worker does not understand; classification can happen after intake.

Recommended transaction sequence:

Read this bot's saved next offset.
Call getUpdates using that offset.
If the returned batch is empty, keep the checkpoint unchanged.
Otherwise begin a database transaction:
  Insert every returned update, ignoring already-stored duplicate keys.
  Save max(update_id in this batch) + 1 as the next offset.
Commit.
Only after commit, issue the next polling request.
Process stored updates with separate workers.

This is design pseudocode, not a complete polling client. It assumes one active polling owner, a transactional durable store, and a uniqueness constraint. If the transaction fails, stop advancement and retry from the saved checkpoint; do not catch the storage error and continue with the larger offset.

If your inbox and checkpoint live in different systems, this transaction is not automatically available. Use a deliberately designed durable handoff and reconciliation process rather than assuming two successful writes are atomic.

Review the crash boundaries

Use these as acceptance tests, not as claims of measured results:

Interruption pointExpected recovery
After receiving a batch, before commitReload the old checkpoint; accept possible repeat delivery
During the transactionNo partial checkpoint should survive a rollback
After commit, before the next pollReload the new checkpoint; stored work remains available
After an external action, before recording completionReconcile or use downstream idempotency; the intake key alone cannot prevent a repeated action

A repeated update should resolve to the existing inbox record. That does not mean the business action finished: a worker must still find and retry stored, unfinished work. Conversely, receiving an update again should not automatically launch another reply or another CRM mutation.

During a deployment, transfer polling ownership explicitly. A forgotten development process or manual diagnostic call using a higher offset can confirm updates outside your storage path. Do not let “read-only troubleshooting” advance production progress.

Limits and the UnifyPort boundary

Telegram documents that incoming updates are retained for no longer than 24 hours. Polling checkpoints do not create an unlimited archive, and lowering an offset cannot restore already confirmed or expired updates. Investigate interruptions as possible loss windows.

For an existing-account or cross-channel inbox, the identity decision is different; see Telegram Bot API webhook vs unified inbound webhook. UnifyPort’s unofficial interface uses normalized events such as message.received, not your bot’s polling offset.

The UnifyPort delivery reference defines its separate acknowledgement contract. Configure signing_secret, verify X-Device-Signature using HMAC-SHA256 over the timestamp, a dot, and raw request bytes, then durably store the event before returning a successful response. Ordinary event retries reuse X-Device-Event-Id. UnifyPort does not provide a REST message-history read API or guaranteed replay of missed payloads. It is not a recovery service for confirmed Bot API updates.

FAQ

Why does getUpdates keep returning the same updates?

Check whether the next request actually uses an offset greater than their update_id, and whether a restart reloads the saved checkpoint. Deduplicate safely rather than discarding the queue.

Should I wait for the AI or CRM job before advancing?

Not if you have durably stored the full update and can retry the job independently. A record kept only in memory is not a durable handoff.

Does this guarantee exactly-once replies?

No. Atomic intake storage prevents one class of lost work; an external send can still succeed before your worker records completion. Design idempotency or reconciliation at that boundary too.

Next step and sources

Audit the polling loop at the database commit boundary. If you instead operate a connected-account receiver, implement the webhook delivery contract without importing Bot API offset logic.

UnifyPort API

Turn messaging integration into a stable product pipeline.

Start by sending through one API, then bring every inbound message back into your business system with standard events.