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 togetUpdates.limit— how many updates onegetUpdatesmay 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
-
PollingSettings( timeout: Int, limit: Int, poll_interval: Int, on_stop: option.Option(fn(error.TelegaError) -> Nil), )
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 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_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