How to Receive Telegram Updates Using Long Polling in Telego

Call Bot.UpdatesViaLongPolling(ctx, params, opts...) to start a long-polling loop that returns a receive-only channel of Update objects, which closes automatically when the context is cancelled.

The Telego library provides a robust, production-ready implementation of Telegram's long-polling mechanism for Go applications. By using the UpdatesViaLongPolling method, you can receive Telegram updates using long polling in Telego without managing the underlying HTTP retry logic, offset tracking, or concurrency controls yourself.

How UpdatesViaLongPolling Works

The UpdatesViaLongPolling method, defined in long_polling.go, orchestrates the entire polling lifecycle through several coordinated steps.

Single Update Source Enforcement

Before starting the loop, Telego checks the bot's internal running state to ensure that only one update source is active at a time. If a webhook or another long-polling instance is already running, the method returns an error immediately【/cache/repos/github.com/mymmrac/telego/main/long_polling.go#L79-L82】.

Configuration and Defaults

The method constructs a longPolling configuration via createLongPolling, applying the following defaults when no options are provided【/cache/repos/github.com/mymmrac/telego/main/long_polling.go#L56-L71】:

  • updateChanBuffer: 100 (channel capacity)
  • updateInterval: 0 (no artificial delay between requests)
  • retryTimeout: 8s (wait time before retrying on error)

If you pass nil for params, Telego automatically applies a default timeout of 8 seconds to the GetUpdatesParams【/cache/repos/github.com/mymmrac/telego/main/long_polling.go#L91-L102】.

The Polling Loop

Telego spawns a goroutine running doLongPolling, which repeatedly calls Bot.GetUpdates (the low-level HTTP wrapper from methods.go) while respecting the supplied context【/cache/repos/github.com/mymmrac/telego/main/long_polling.go#L104-L107】.

Inside the loop, the following logic applies:

  1. Context cancellation: The loop exits immediately when the caller-provided ctx is cancelled【/cache/repos/github.com/mymmrac/telego/main/long_polling.go#L17-L20】.
  2. Error handling: If GetUpdates returns an error, Telego checks whether it is fatal or if retries are disabled (retryTimeout == 0). In either case, the channel closes; otherwise, it logs the error and sleeps for retryTimeout before retrying【/cache/repos/github.com/mymmrac/telego/main/long_polling.go#L24-L34】.
  3. Offset tracking: For each update with UpdateID >= params.Offset, the offset is advanced and the update (augmented with the request context via WithContext) is sent into the channel【/cache/repos/github.com/mymmrac/telego/main/long_polling.go#L36-L47】.
  4. Rate limiting: If updateInterval is greater than zero, the loop sleeps for that duration after each successful batch【/cache/repos/github.com/mymmrac/telego/main/long_polling.go#L50-L52】.

When the context finishes, the channel closes and the bot's running flag is reset【/cache/repos/github.com/mymmrac/telego/main/long_polling.go#L11-L15】.

Customizing Long Polling Behavior

Telego provides three functional options to tune the polling mechanism:

  • WithLongPollingUpdateInterval(d time.Duration): Sets a pause between consecutive GetUpdates calls. Useful to reduce request rate when you don't need minimum latency【/cache/repos/github.com/mymmrac/telego/main/long_polling.go#L30-L42】.
  • WithLongPollingRetryTimeout(d time.Duration): Changes the back-off after a recoverable error. Set to 0 to disable retries and close the channel on first error【/cache/repos/github.com/mymmrac/telego/main/long_polling.go#L45-L56】.
  • WithLongPollingBuffer(buf uint): Changes the internal channel capacity from the default 100【/cache/repos/github.com/mymmrac/telego/main/long_polling.go#L59-L66】.

Pass these options variadically to UpdatesViaLongPolling:

updates, err := bot.UpdatesViaLongPolling(ctx, nil,
    telego.WithLongPollingUpdateInterval(2*time.Second),
    telego.WithLongPollingRetryTimeout(5*time.Second),
    telego.WithLongPollingBuffer(200))

Graceful Shutdown Handling

Because the update channel is tied to a context.Context, cancelling the context (e.g., on SIGINT) stops the polling loop and closes the channel. The repository includes a complete example demonstrating how to wait for the channel to drain before stopping the handler: examples/graceful_shutdown_long_polling/main.go【/cache/repos/github.com/mymmrac/telego/main/examples/graceful_shutdown_long_polling/main.go#L14-L78】.

Complete Working Examples

Minimal Long Polling Bot

This example shows the bare minimum to receive Telegram updates using long polling in Telego:

package main

import (
	"context"
	"fmt"
	"os"

	"github.com/mymmrac/telego"
)

func main() {
	// Bot token from the environment
	bot, err := telego.NewBot(os.Getenv("TOKEN"))
	if err != nil {
		panic(err)
	}
	defer bot.Stop() // clean shutdown

	// Start long-polling; nil params → default timeout (8 s)
	updates, err := bot.UpdatesViaLongPolling(context.Background(), nil)
	if err != nil {
		panic(err)
	}

	// Consume updates
	for update := range updates {
		fmt.Printf("Received update %d from chat %d\n",
			update.UpdateID, update.Message.Chat.ID)
		// …your handling logic here…
	}
}

Key points:

  • UpdatesViaLongPolling returns a read-only <-chan Update.
  • The channel closes automatically when the supplied context is cancelled or an unrecoverable error occurs.

Production-Ready Bot with Graceful Shutdown

This example mirrors the official graceful_shutdown_long_polling sample【/cache/repos/github.com/mymmrac/telego/main/examples/graceful_shutdown_long_polling/main.go#L14-L78】:

package main

import (
	"context"
	"fmt"
	"os"
	"os/signal"
	"time"

	"github.com/mymmrac/telego"
	th "github.com/mymmrac/telego/telegohandler"
)

func main() {
	bot, _ := telego.NewBot(os.Getenv("TOKEN"))
	defer bot.Stop()

	// Context that ends on Ctrl+C
	ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
	defer cancel()

	// Long-polling with custom options
	updates, _ := bot.UpdatesViaLongPolling(
		ctx,
		nil,
		telego.WithLongPollingUpdateInterval(2*time.Second), // slower request rate
		telego.WithLongPollingRetryTimeout(5*time.Second),  // quicker retries
	)

	// Attach a handler (optional, but shows integration with telegohandler)
	handler, _ := th.NewBotHandler(bot, updates)
	handler.Handle(func(c *th.Context, upd telego.Update) error {
		fmt.Printf("Processing update %d\n", upd.UpdateID)
		return nil
	})

	// Run the handler in its own goroutine
	go func() { _ = handler.Start() }()

	// Wait until the signal is received
	<-ctx.Done()
	fmt.Println("Shutting down…")
	// Give the handler a chance to finish current work
	shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer shutdownCancel()
	_ = handler.StopWithContext(shutdownCtx)
}

This implementation demonstrates:

  • Signal handling for graceful shutdown.
  • Custom polling intervals to control request frequency.
  • Integration with telegohandler for structured update processing.

Summary

  • Primary method: Use Bot.UpdatesViaLongPolling(ctx, params, opts...) from long_polling.go to receive Telegram updates using long polling in Telego.
  • Concurrency safety: The method enforces a single active update source via an internal running state check.
  • Configurable behavior: Tune the polling loop with WithLongPollingUpdateInterval, WithLongPollingRetryTimeout, and WithLongPollingBuffer.
  • Context-driven lifecycle: The polling loop respects context cancellation, automatically closing the update channel and resetting internal state on shutdown.
  • Graceful shutdown: Reference the official example at examples/graceful_shutdown_long_polling/main.go for production-ready signal handling.

Frequently Asked Questions

What is the default timeout for long polling in Telego?

When you pass nil for the params argument to UpdatesViaLongPolling, Telego automatically creates a GetUpdatesParams struct with a default timeout of 8 seconds. This is implemented in long_polling.go where a shallow copy of the parameters is prepared before entering the polling loop【/cache/repos/github.com/mymmrac/telego/main/long_polling.go#L91-L102】.

How do I stop long polling gracefully?

To stop long polling gracefully, cancel the context.Context you passed to UpdatesViaLongPolling. When the context is cancelled, the internal doLongPolling goroutine detects this, closes the update channel, and resets the bot's running state【/cache/repos/github.com/mymmrac/telego/main/long_polling.go#L17-L20】. For production applications, use signal.NotifyContext to trigger shutdown on SIGINT and drain remaining updates before exiting, as shown in the examples/graceful_shutdown_long_polling/main.go sample【/cache/repos/github.com/mymmrac/telego/main/examples/graceful_shutdown_long_polling/main.go#L14-L78】.

Can I use both webhook and long polling simultaneously?

No, Telego explicitly prevents simultaneous use of webhooks and long polling. The UpdatesViaLongPolling method checks the bot's internal running atomic state at startup and returns an error if another update source (such as a webhook or existing long-polling loop) is already active【/cache/repos/github.com/mymmrac/telego/main/long_polling.go#L79-L82】. This design ensures thread safety and prevents duplicate update processing.

How do I handle errors during long polling?

Telego's long-polling implementation includes automatic retry logic for transient errors. By default, if GetUpdates returns a non-fatal error, the loop logs the error and sleeps for the retryTimeout duration (default 8 seconds) before retrying【/cache/repos/github.com/mymmrac/telego/main/long_polling.go#L32-L34】. You can customize this behavior using WithLongPollingRetryTimeout—setting it to 0 disables retries and causes the channel to close immediately on the first error【/cache/repos/github.com/mymmrac/telego/main/long_polling.go#L45-L56】. Fatal errors (such as context cancellation) always terminate the loop immediately【/cache/repos/github.com/mymmrac/telego/main/long_polling.go#L24-L31】.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →