When a client is being actively used, events will occur that affect the current user and that they must learn about as soon as possible, e.g. when a new message is received. To eliminate the need for the client itself to periodically download these events, there is an update delivery mechanism in which the server sends the user notifications over one of its available connections with the client.
Update events are sent to an authorized user into the last active connection (except for connections needed for downloading / uploading files).
So to start receiving updates the client needs to init connection and call API method, e.g. to fetch current state.
All events are received from the socket as a sequence of TL-serialized Updates objects, which might be optionally gzip-compressed in the same way as responses to queries.
Each Updates object may contain single or multiple Update objects, representing different events happening.
In order to apply all updates in precise order and to guarantee that no update is missed or applied twice there is seq
attribute in Updates constructors, and pts
(with pts_count
) or qts
attributes in Update constructors. The client must use those attributes values in combination with locally stored state to correctly apply incoming updates.
When a gap in updates sequence occurs, it must be filled via calling one of the API methods. More below »
As said earlier, each payload with updates has a TL-type Updates. It can be seen from the schema below that this type has several constructors.
updatesTooLong#e317af7e = Updates;
updateShort#78d4dec1 update:Update date:int = Updates;
updateShortMessage#313bc7f8 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true id:int user_id:long message:string pts:int pts_count:int date:int fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?long reply_to:flags.3?MessageReplyHeader entities:flags.7?Vector<MessageEntity> ttl_period:flags.25?int = Updates;
updateShortChatMessage#4d6deea5 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true id:int from_id:long chat_id:long message:string pts:int pts_count:int date:int fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?long reply_to:flags.3?MessageReplyHeader entities:flags.7?Vector<MessageEntity> ttl_period:flags.25?int = Updates;
updateShortSentMessage#9015e101 flags:# out:flags.1?true id:int pts:int pts_count:int date:int media:flags.9?MessageMedia entities:flags.7?Vector<MessageEntity> ttl_period:flags.25?int = Updates;
updatesCombined#725b04c3 updates:Vector<Update> users:Vector<User> chats:Vector<Chat> date:int seq_start:int seq:int = Updates;
updates#74ae4240 updates:Vector<Update> users:Vector<User> chats:Vector<Chat> date:int seq:int = Updates;
updatesTooLong indicates that there are too many events pending to be pushed to the client, so one needs to fetch them manually.
Events inside updateShort constructors, normally, have lower priority and are broadcast to a large number of users, i.e. one of the chat participants started entering text in a big conversation (updateChatUserTyping).
The updateShortMessage, updateShortSentMessage and updateShortChatMessage constructors are redundant but help significantly reduce the transmitted message size for 90% of the updates. They should be transformed to updateShort upon receiving.
Two remaining constructors updates and updatesCombined are part of the Updates sequence. Both of them have seq
attribute, which indicates the remote Updates state after the generation of the Updates, and seq_start
indicates the remote Updates state after the first of the Updates in the packet is generated. For updates, seq_start
attribute is omitted, because it is assumed that it is always equal to seq
.
Each event related to a message box (message created, message edited, message deleted, etc) is identified by a unique autoincremented pts, or qts in case of secret chat updates, certain bot updates, etc.
Each message box can be considered as some server-side DB table that stores messages and events associated with them. All boxes are completely independent, and each pts sequence is tied to just one box (see below).
Update object may contain info about multiple events (for example, updateDeleteMessages).
That's why all single updates might have pts_count parameter indicating the number of events contained in the received update (with some exceptions, in this case, the pts_count is considered to be 0
).
Each channel and supergroup has its message box and its event sequence as a result; private chats and legacy groups of one user have another common event sequence. Secret chats, certain bot events and other kinds of updates have yet another common secondary event sequence.
To recap, the client has to take care of the integrity of the following sequences to properly handle updates:
The common update state is represented by the updates.State constructor. When the user logs in for the first time, call to updates.getState has to be made to store the latest update state (which will not be the absolute initial state, just the latest state at the current time). The common update state can also be fetched from updates.differenceTooLong.
The channel update state is represented simply by the pts of the event sequence: when first logging in, the initial channel state can be obtained from the dialog constructor when fetching dialogs, from the full channel info, or it can be received as an updateChannelTooLong update.
The secondary update state is represented by the qts of the secret event sequence, it is contained in the updates.State of the common update state.
The Updates sequence state is represented by the date and seq of the Updates sequence, it is contained in the updates.State of the common update state.
Update handling in Telegram clients consists of receiving events, making sure there were no gaps and no events were missed based on the locally stored state of the correspondent event sequence, and then updating the locally stored state based on the parameters received.
When the client receives payload with serialized updates, first of all, it needs to walk through all of the nested Update objects and check if they belong to any of message box sequences (have pts
or qts
parameters). Those updates need to be handled separately according to corresponding local state and new pts
/qts
values. Details below »
After message box updates are handled, if there are any other updates remaining the client needs to handle them with respect to seq
. Details below »
pts
: checking and applyingHere, local_pts
will be the local state, pts
will be the remote state, pts_count
will be the number of events in the update.
If local_pts + pts_count === pts
, the update can be applied.
If local_pts + pts_count > pts
, the update was already applied, and must be ignored.
If local_pts + pts_count < pts
, there's an update gap that must be filled.
For example, let's assume the client has the following local state for the channel 123456789
:
local_pts = 131
Now let's assume an updateNewChannelMessage from channel 123456789
is received with pts = 132
and pts_count=1
.
Since local_pts + pts_count === pts
, the total number of events since the last stored state is, in fact, equal to pts_count
: this means the update can be safely accepted and the remote pts
applied:
local_pts = 132
Since:
pts
indicates the server state after the new channel message events are generatedpts_count
indicates the number of events in the new channel updatepts_before = pts - pts_count = 131
, which is, in fact, equal to our local state.Now let's assume an updateNewChannelMessage from channel 123456789
is received with pts = 132
and pts_count=1
.
Since local_pts + pts_count > pts
(133 > 132
), the update is skipped because we've already handled this update (in fact, our current local_pts
was set by this same update, and it was resent twice due to network issues or other issues).
Now let's assume an updateDeleteChannelMessages from channel 123456789
is received with pts = 140
and pts_count=5
.
Since local_pts + pts_count < pts
(137 < 140
), this means that updates were missed, and the gap must be recovered.
The whole process is very similar for secret chats and certain bot updates, but there is qts
instead of pts
, and events are never grouped, so it's assumed that qts_count
is always equal to 1.
seq
: checking and applyingOn top level when handling received updates and updatesCombined there are three possible cases:
If local_seq + 1 === seq_start
, the updates can be applied.
If local_seq + 1 > seq_start
, the updates were already applied, and must be ignored.
If local_seq + 1 < seq_start
, there's an updates gap that must be filled (updates.getDifference must be used as with common and secret event sequences).
If the updates were applied, local Updates state must be updated with seq
and date
from the constructor.
For all the other Updates type constructors there is no need to check seq
or change a local state.
To do this, updates.getDifference (common/secret state) or updates.getChannelDifference (channel state) with the respective local states must be called. These methods should also be called on startup, to fetch new updates (preferably with some flags to reduce server load, see the method's docs). Manually obtaining updates is also required in the following situations:
When calling updates.getDifference if the updates.differenceSlice constructor is returned in response, the full difference was too large to be received in one request. The intermediate status, intermediate_state, must be saved on the client and the query must be repeated, using the intermediate status as the current status.
To fetch the updates difference of a channel, updates.getChannelDifference is used.
If the difference is too large to be received in one request, the final
flag of the result is not set (see docs).
The intermediate status, represented by the pts, must be saved on the client and the query must be repeated, using the intermediate status as the current status.
For performance reasons and for better user experience, client can set maximum gap size to be filled: pts_total_limit
parameter of updates.getDifference and limit
parameter for updates.getChannelDifference can be used.
If the gap is too large and there are too many updates to fetch, a *TooLong
constructor will be returned. In this case, the client must re-fetch the state, re-start fetching updates from that state and follow the instructions that can be found here.
It is recommended to use limit 10-100
for channels and 1000-10000
otherwise.
Implementations also have to take care to postpone updates received via the socket while filling gaps in the event and Update sequences, as well as avoid filling gaps in the same sequence.
Example implementations: tdlib, MadelineProto.
An interesting and easy way this can be implemented, instead of using various locks, is by running background loops, like in MadelineProto ».
If a client does not have an active connection at the time of an event, PUSH Notifications will also be useful.