telega/polling

Long polling implementation for Telegram Bot API.

This module provides long polling as an alternative to webhooks for receiving updates. A polling worker actor continuously fetches updates from Telegram and dispatches them to the bot’s message handlers.

Supervised mode (recommended)

When using telega.start(), the polling worker is automatically started inside the supervision tree as a Permanent child. No manual setup is needed:

let assert Ok(_bot) =
  telega.new(api_client)
  |> telega.router(router)
  |> telega.start()

process.sleep_forever()

Use telega.polling(settings) on the builder to customize timeout, limit and poll interval before calling telega.start().

Manual mode

For advanced use cases (custom offsets, separate lifecycle management), start polling manually:

let assert Ok(poller) =
  polling.start_polling_default(
    client: telega.get_api_config(bot),
    bot: telega.get_bot_subject_internal(bot),
  )

Concurrency and backpressure

Updates are dispatched to the bot without waiting for their handlers to finish, so a slow handler in one chat never holds up another chat or the next getUpdates. Ordering is preserved: the poller sends updates in the order Telegram returned them, and each chat has a single actor, so updates of the same chat are still handled one after another.

To keep a burst from piling up without bound, the worker stops fetching once limit updates are in flight and resumes as soon as one settles — i.e. at most one getUpdates batch is being handled at a time. Tune it with telega.polling(PollingSettings(..default_settings(), limit:)).

Error handling

The worker retries forever with exponential backoff capped at one minute, so an outage of any length — a network partition, a Telegram incident, a webhook that is still registered (409) — is survived rather than turned into a stopped bot.

Only errors that cannot resolve themselves stop polling: 401 (invalid token) and 404 (bot deleted). Those invoke the optional on_stop callback.

deleteWebhook is called by the worker itself, lazily and with the same backoff, instead of in its init — a failing call there would have made the restart fail too, and taken the supervision tree down with it.

Types

Opaque type representing a running poller instance.

The status is not stored here — it lives in the worker, and asking the Poller record for it (as this used to) only ever returned the value captured at construction.

pub opaque type Poller

Status of the poller

pub type PollerStatus {
  Starting
  Running
  Stopped
  Failed(String)
}

Constructors

  • Starting
  • Running
  • Stopped
  • Failed(String)

Messages for the polling worker actor.

Opaque: the constructors are the worker’s own protocol (SetSelf and InjectUpdates in particular are internal plumbing). Drive a poller with stop, stop_worker and get_status instead.

pub opaque type PollingMessage

How the supervised polling worker fetches updates.

Build it with default_settings() and override what you need:

telega.new(api_client)
|> telega.polling(polling.PollingSettings(
  ..polling.default_settings(),
  limit: 10,
))
  • timeout — long-poll timeout in seconds sent to getUpdates.
  • limit — how many updates one getUpdates may return, which also bounds how many are in flight at once (the worker stops fetching until the bot acks them).
  • poll_interval — pause in milliseconds between polls.
  • on_stop — called when polling stops for good (an invalid token, a deleted bot); everything else is retried forever.
pub type PollingSettings {
  PollingSettings(
    timeout: Int,
    limit: Int,
    poll_interval: Int,
    on_stop: option.Option(fn(error.TelegaError) -> Nil),
  )
}

Constructors

Values

pub fn calculate_new_offset(
  updates: List(types.Update),
  current_offset: Int,
) -> Int

Calculate the next offset based on received updates

pub fn default_settings() -> PollingSettings

The polling settings a bot uses when it does not ask for others: a 30 second long poll, 100 updates per batch, a 1 second interval and no on_stop hook.

pub fn get_config_info(
  poller: Poller,
) -> #(Int, Int, List(String), Int)

Get the polling configuration metadata

pub fn get_status(poller: Poller) -> PollerStatus

Get the current status of the poller, as the worker itself sees it.

A worker that is gone — stopped, or ended by a fatal error — reports Stopped; the error behind a fatal stop is delivered to the on_stop callback, not through the status.

pub fn is_running(poller: Poller) -> Bool

Check if poller is running

pub fn start_polling(
  client client: client.TelegramClient,
  bot bot: process.Subject(bot.BotMessage),
  timeout timeout: Int,
  limit limit: Int,
  allowed_updates allowed_updates: List(String),
  poll_interval poll_interval: Int,
) -> Result(Poller, error.TelegaError)

Start polling with the given client and bot subject.

pub fn start_polling_default(
  client client: client.TelegramClient,
  bot bot: process.Subject(bot.BotMessage),
) -> Result(Poller, error.TelegaError)

Start polling with default configuration.

pub fn start_polling_with_notify(
  client client: client.TelegramClient,
  bot bot: process.Subject(bot.BotMessage),
  timeout timeout: Int,
  limit limit: Int,
  allowed_updates allowed_updates: List(String),
  poll_interval poll_interval: Int,
  on_stop on_stop: fn(error.TelegaError) -> Nil,
) -> Result(Poller, error.TelegaError)

Start polling with a notification callback for when polling stops due to errors. The callback will be invoked with the error that caused polling to stop.

pub fn start_polling_with_offset(
  client client: client.TelegramClient,
  bot bot: process.Subject(bot.BotMessage),
  offset offset: Int,
  timeout timeout: Int,
  limit limit: Int,
  allowed_updates allowed_updates: List(String),
  poll_interval poll_interval: Int,
) -> Result(Poller, error.TelegaError)

Start polling with a custom offset.

pub fn stop(poller: Poller) -> Nil

Stop polling

pub fn stop_worker(
  worker worker: process.Subject(PollingMessage),
) -> Nil

Stop a supervised polling worker by its subject.

Sends StopPolling, which makes the worker stop fetching new updates after its current batch. Used by graceful shutdown to halt intake before draining.

pub fn supervised(
  client client: client.TelegramClient,
  bot bot: process.Subject(bot.BotMessage),
  timeout timeout: Int,
  limit limit: Int,
  allowed_updates allowed_updates: List(String),
  poll_interval poll_interval: Int,
  on_stop on_stop: option.Option(fn(error.TelegaError) -> Nil),
  name name: process.Name(PollingMessage),
) -> supervision.ChildSpecification(
  process.Subject(PollingMessage),
)

Create a ChildSpecification for running polling inside a supervision tree. The polling worker will automatically delete the webhook and start polling.

pub fn wait_finish(poller: Poller) -> Nil

Wait for the poller to finish This function blocks indefinitely until the polling worker stops

Search Document