The update lifecycle in aiogram

This article describes the key aspects of how Telegram bots work and will be useful for both beginners who are just starting out and for advanced developers. Specifically, we're talking about the aiogram framework, but all the ideas presented can be seen anywhere. I assure you that after reading this, it will be much easier for you to understand all the details of the mechanism as a single process

Article Contents

  1. The concept of an update in Telegram
  2. Methods of receiving updates
  3. Processing of updates
  4. Router and dispatcher
  5. Middleware
  6. Filter
  7. Handler

Local terms All terms should be understood in the context of Telegram and aiogram. Similar concepts can be found in many places, so don't take them as universal definitions for everything!


Update

💡 An Update — is an object with data that appears as a result of a certain action from the client's side, for example, a user or a bot

Examples for better understanding

Why don't bots see each other's messages? A trivial but simple example. Imagine there are two bots in a group. The first one sends the message «chicken», the second one replies «egg». It seems normal, but what if you configure the first bot to reply to «egg» with «chicken» again? Then you get an infinite loop of messages chicken, egg, chicken, egg... This is why Telegram has such a restriction

The term update should be used as a general concept that speaks about some events and their processing. A good sentence would be: «My bot doesn't catch updates» – this literally means that the bot does not react to the user's actions in any way

Types of updates

An update should be understood as some event. In turn, each event that has occurred has a defined update type. This is done so that one can easily understand the type of event (new message, user deletion, button click, etc) and standardize the data that will be included in the update. Because it is better to have a certain type of update with predefined fields that can be in it, rather than receiving a universal update and trying to figure out what happened

The most commonly used update types


Methods of receiving updates

You can manage a bot without running it It is often said «send from under the token» – that is, take the bot's token and send the necessary request to the Telegram Bot API. To do this, you don't need to run the update receiving mechanism, but its operation will not interfere in any way

Which update receiving mechanism should be chosen? Long-polling or Webhook — that is the question. To answer it, one should analyze the load on the bot, the need for its scaling. If in the future the bot will be popular only locally, then polling will be enough. A webhook will make sense, for example, for organizing a high-load platform or for a quality multibot. But the method of receiving updates can always be changed!


Processing of updates

The full path of update processing includes the following stages

Then the update goes through the so-called chain of processors

Knowledge vs. trial and error Not every person who uses aiogram understands how an update passes through all these objects. It is important to be able to see the big picture. Then you will be able to quickly understand where and how to implement this or that bot logic!


Router and dispatcher

We received an update, what's next? Obviously, the bot should respond to some things and not to others — that is, filter the events that are relevant to it. Here, registrations and processors come into play — they are the foundation of the entire filtering system

Registrations are important One of the most common mistakes among beginners is precisely the misunderstanding of the nature of registrations. Keep this in mind — and you will have fewer thoughts about why this update first goes there and not here

Now let's move on to the organization of catching updates, namely to routers. Perhaps you have only used aiogram 2.X and routers scare you, it's not clear why they are needed. Then I will try to change your mind!

Purely theoretically, one dispatcher as a router would be enough to handle all events. But using routers has its advantages

  1. Modularity:

    • Each router is responsible for a specific functionality of the bot
    • This improves code organization and makes it more structured
    • Examples:
      • The admin router contains handlers only for administrators
      • The user router processes commands from regular users
      • The payment router is responsible for all payment-related operations
    • This approach simplifies code navigation and maintenance
  2. Registration:

    • Instead of registering each processor in the dispatcher, you can manage entire branches of processors
    • This makes it easier to set the priority of update processing
    • Example:
      • First, updates from the user router are processed
      • Then, if the update is not processed, it goes to the admin router
    • Important: the order of registration of processors within each router also matters
  3. Flexibility:

    • You can easily enable or disable entire groups of functionality
    • This is useful for:
      • Testing new features without affecting the main code
      • Implementing «red buttons» to quickly disable certain functions
      • Creating multibots where different sets of functions can be enabled depending on the context
    • Example: you can temporarily disable the payment router for maintenance without affecting other bot functions
  4. Privacy:

    • Each router can have its own filters and middlewares
    • This allows:
      • Setting access rights checks once for the entire set of handlers
      • Logging actions within a specific functionality
    • Examples:
      • The admin router can have a filter that checks for administrative rights for all its handlers
      • The payment router can log all financial transactions without affecting other parts of the bot
  5. Hierarchy:

    • Routers can contain other routers, creating a tree-like structure
    • This allows:
      • Implementing complex update processing systems
      • Creating flexible access rights systems
    • Example:
      • The main user router can contain sub-routers beginner, advanced, premium
      • Each sub-router has its own set of commands and functions available for the corresponding user level

None of these advantages exist with a single dispatcher But don't think badly of the dispatcher. It serves more general purposes. The text above is aimed at convincing you not to use only one dispatcher in your bot (unless, of course, it's a primitive functionality). P.S. Switch to aiogram 3.X

How an update propagates through routers, their filters, and handlers (middlewares are not shown)


Middleware

🔐 A Middleware — is an object that acts as an intermediary link between processors and works with an update at the level of its type [1]. It has two types of registration [2]. Its use comes down to calling or not calling the next processor in the chain in the right place (according to the bot's logic) [3]. Middleware provides the opportunity for pre, «inner», post, and outgoing processing of updates [4]. They can be implemented as classes or functions (the first option is more common)

[1] Middleware registration is only possible at the update type level, so the content of the internal data of the update does not play any role for it (but no one said that during processing, middleware cannot act as a filter for specific data in the update)

[2] Let's deal with the two types of registrations, for this, let's look at the figure below (original)

Organization of middleware

As you can see, there are outer middleware and middleware, in simple terms — outer and inner. It would be correct to think that they have different registration priorities for an update

What is the difference between these registrations?

Registration for any update A middleware can be registered in such a way that any updates (of the update type) will come to it. The type of registration does not affect this in any way. Formally, outer and inner registration for the update type are different things, but in fact, they will work the same. P.S. It's better to use outer for this

[3] About calling the next processor in the chain. For example, let's look at this middleware code (original)

from aiogram import BaseMiddleware
from aiogram.types import Message


class CounterMiddleware(BaseMiddleware):
    def __init__(self) -> None:
        self.counter = 0

    async def __call__(
        self,
        handler: Callable[[Message, Dict[str, Any]], Awaitable[Any]],
        event: Message,
        data: Dict[str, Any]
    ) -> Any:
        self.counter += 1
        data['counter'] = self.counter
        return await handler(event, data)

Special attention should be paid to the await handler(event, data) call. This action passes the update further down the chain of processors. If there is a case in the middleware (for example, under certain conditions) where await handler(event, data) is not called, it will mean that the update is discarded/dropped. That is, none of the processors below will get access to the update as if it never existed. Therefore, we can say that in general, the job of a middleware is to pass data from the update somewhere and/or drop it under certain filtering conditions

[4] About the types of processing

Dependency Injection

In aiogram, the Dependency Injection mechanism allows processors to receive dependencies (data necessary for the call) from other objects that have them or received them in the same way before. These dependencies can be obtained in different ways depending on the type of processor:

Delegator middleware

This is an author's name from the word delegate (to pass on), which describes middlewares that pass data to processors using Dependency Injection. Perhaps, the delegator middleware is mainly used to obtain a database session so that the processors below have a convenient interface for querying it. Also, data processed in a certain way that depends on the update can be passed (eg something filtered from the update data or data on the bot's side related to the user). To use this mechanism in the context of middlewares, you need to add data through the data argument. This is a dictionary whose data is passed to the processors below


Filter

🕸 A Filter — is probably the widest range of processors. However, they are divided into two main types: custom and out-of-the-box. Here we will discuss custom ones, and we will discuss the rest later. A filter can be:

When called, filters must return a boolean or a dictionary, which determines whether the update has passed the filtration (it only fails on False)

Unlike middleware, the task of a filter is simply to filter the update. But sometimes, the filtering logic can be more complex than a simple comparison. For example, it makes sense to write a custom filter if you need to check whether the update belongs to a group admin

An example of a simple filter that compares the message text with a given one (original)

from aiogram import Router
from aiogram.filters import Filter
from aiogram.types import Message

router = Router()


class MyFilter(Filter):
    def __init__(self, my_text: str) -> None:
        self.my_text = my_text

    async def __call__(self, message: Message) -> bool:
        return message.text == self.my_text


@router.message(MyFilter("hello"))
async def my_handler(message: Message): 
    ...

Combinations of filters

If several filters are set during registration, logical AND will be used (the update must pass all of them). It is also allowed to have several registrations for one processor, in which case logical OR will be used (it is enough for the update to pass the filters of at least one registration). You can also use the magic filter syntax for combinations

Delegator filter

Yes, they can pass data using Dependency Injection. This is possible if the filter returns a dictionary. Then its data can be obtained in the following processors. This is very convenient for obtaining certain data from an update


Handler

🦾 A Handler — is a function registered to process updates. It is the final processor (the update goes no further) in which it is customary to interact with the user. The function should not return anything and can be registered both with and without filters. But at the same time, a handler can be implemented as a subclass

Ghost handler A common cause of errors for beginners is a handler without filters. All updates for which it is registered will go into such a handler. Therefore, if you understand that an update is going somewhere it shouldn't, remember if you have such a handler. In general, its use is not bad practice, you just need to work with it carefully

Not much can be said about the handler itself, but we can talk about the filters that work with it. We discussed custom filters above, now it's time for the built-in ones

Built-in filters for handlers

Found a mistake? Feedback: hello@shaonis.space