Skip to main content
Middleware in Telebot lets you intercept, inspect, and short-circuit handler execution. A middleware wraps a handler function and decides whether to call the next one in the chain.

The MiddlewareFunc Type

// MiddlewareFunc represents a middleware processing function,
// which gets called before the endpoint group or specific handler.
type MiddlewareFunc func(HandlerFunc) HandlerFunc
A middleware receives the next HandlerFunc in the chain and returns a new HandlerFunc that can execute logic before, after, or instead of calling next.

Basic Pattern

func MyMiddleware(next tele.HandlerFunc) tele.HandlerFunc {
    return func(c tele.Context) error {
        // logic before the handler
        err := next(c)
        // logic after the handler
        return err
    }
}
To short-circuit (skip the handler entirely), just return without calling next:
func MyMiddleware(next tele.HandlerFunc) tele.HandlerFunc {
    return func(c tele.Context) error {
        if someCondition {
            return nil // skip handler silently
        }
        return next(c)
    }
}

Passing Data Through the Chain

Use c.Set and c.Get to store and retrieve arbitrary values within the same request lifetime:
// Set saves data in the context.
func (c Context) Set(key string, val interface{})

// Get retrieves data from the context.
func (c Context) Get(key string) interface{}
func AuthMiddleware(next tele.HandlerFunc) tele.HandlerFunc {
    return func(c tele.Context) error {
        user, err := db.FindUser(c.Sender().ID)
        if err != nil {
            return err
        }
        // make the user available to the handler
        c.Set("user", user)
        return next(c)
    }
}

func onProfile(c tele.Context) error {
    user := c.Get("user").(*User)
    return c.Send("Hello, " + user.Name)
}

Attaching Middleware

There are three scopes at which middleware can be applied.

Global — b.Use

Applies to every handler registered on the bot:
// Use adds middleware to the global bot chain.
func (b *Bot) Use(middleware ...MiddlewareFunc)
b.Use(middleware.Logger())
b.Use(middleware.Recover())
b.Use(AuthMiddleware)

Group

Applies only to handlers registered through that group:
// Group is a separated group of handlers, united by the general middleware.
type Group struct { ... }

// Use adds middleware to the chain.
func (g *Group) Use(middleware ...MiddlewareFunc)

// Handle adds endpoint handler to the bot, combining group's middleware.
func (g *Group) Handle(endpoint interface{}, h HandlerFunc, m ...MiddlewareFunc)
adminGroup := b.Group()
adminGroup.Use(AdminOnly(adminIDs...))
adminGroup.Handle("/ban", onBan)
adminGroup.Handle("/broadcast", onBroadcast)

Per-handler

Passed directly to b.Handle or g.Handle as variadic arguments after the handler:
// Handle lets you set the handler for some command name or one of the
// supported endpoints. It also applies middleware if such is passed.
func (b *Bot) Handle(endpoint interface{}, h HandlerFunc, m ...MiddlewareFunc)
b.Handle("/ban", onBan, middleware.Whitelist(adminIDs...))

Middleware ordering

Middleware runs in registration order. For global middleware registered with b.Use, the first one registered is the outermost wrapper — it executes first on the way in and last on the way out.
b.Use(First)   // runs first
b.Use(Second)  // runs second
b.Use(Third)   // runs third, closest to the handler

Built-in Middleware

Telebot ships a middleware subpackage (gopkg.in/telebot.v4/middleware) with ready-to-use middleware.

Logger

Logs every incoming update as indented JSON:
// Logger returns a middleware that logs incoming updates.
// If no custom logger is provided, log.Default() will be used.
func Logger(logger ...*log.Logger) tele.MiddlewareFunc
b.Use(middleware.Logger())

// with a custom logger
b.Use(middleware.Logger(myLogger))

Recover

Catches panics in handlers and converts them to errors:
// Recover returns a middleware that recovers a panic happened in the handler.
func Recover(onError ...RecoverFunc) tele.MiddlewareFunc
b.Use(middleware.Recover())

// with a custom error handler
b.Use(middleware.Recover(func(err error, c tele.Context) {
    log.Printf("panic in handler: %v", err)
    c.Send("Something went wrong, please try again.")
}))

AutoRespond

Automatically calls c.Respond() for every callback query, preventing the Telegram “loading” spinner:
// AutoRespond returns a middleware that automatically responds
// to every callback.
func AutoRespond() tele.MiddlewareFunc
b.Use(middleware.AutoRespond())

Whitelist / Blacklist

Allow or block specific users by chat ID:
// Whitelist returns a middleware that skips the update for users
// NOT specified in the chats field.
func Whitelist(chats ...int64) tele.MiddlewareFunc

// Blacklist returns a middleware that skips the update for users
// specified in the chats field.
func Blacklist(chats ...int64) tele.MiddlewareFunc
// Only allow admins
b.Handle("/admin", onAdmin, middleware.Whitelist(123456789, 987654321))

// Block spammers globally
b.Use(middleware.Blacklist(spammerIDs...))

Restrict (custom in/out)

For more control, use Restrict to define separate handlers for matched and non-matched chats:
// RestrictConfig defines config for Restrict middleware.
type RestrictConfig struct {
    // Chats is a list of chats that are going to be affected.
    Chats []int64

    // In is called when the chat IS in the list.
    In tele.HandlerFunc

    // Out is called when the chat is NOT in the list.
    Out tele.HandlerFunc
}

func Restrict(v RestrictConfig) tele.MiddlewareFunc
b.Use(middleware.Restrict(middleware.RestrictConfig{
    Chats: premiumUserIDs,
    In:    next, // let premium users through
    Out: func(c tele.Context) error {
        return c.Send("This feature is for premium users only.")
    },
}))

IgnoreVia

Skips updates that were sent “via” an inline bot:
// IgnoreVia returns a middleware that ignores all "sent via" messages.
func IgnoreVia() tele.MiddlewareFunc

Real-World Examples

Rate limiting middleware

func RateLimit(limit int, window time.Duration) tele.MiddlewareFunc {
    type entry struct {
        count  int
        window time.Time
    }
    var (
        mu    sync.Mutex
        users = make(map[int64]*entry)
    )

    return func(next tele.HandlerFunc) tele.HandlerFunc {
        return func(c tele.Context) error {
            id := c.Sender().ID
            now := time.Now()

            mu.Lock()
            e, ok := users[id]
            if !ok || now.After(e.window) {
                users[id] = &entry{count: 1, window: now.Add(window)}
                mu.Unlock()
                return next(c)
            }
            e.count++
            if e.count > limit {
                mu.Unlock()
                return c.Send("Too many requests. Please slow down.")
            }
            mu.Unlock()
            return next(c)
        }
    }
}

// Allow 5 messages per 10 seconds per user
b.Use(RateLimit(5, 10*time.Second))

Admin-only middleware

func AdminOnly(adminIDs ...int64) tele.MiddlewareFunc {
    set := make(map[int64]struct{}, len(adminIDs))
    for _, id := range adminIDs {
        set[id] = struct{}{}
    }
    return func(next tele.HandlerFunc) tele.HandlerFunc {
        return func(c tele.Context) error {
            if _, ok := set[c.Sender().ID]; !ok {
                return c.Send("Access denied.")
            }
            return next(c)
        }
    }
}

adminGroup := b.Group()
adminGroup.Use(AdminOnly(123456789))
adminGroup.Handle("/ban", onBan)
adminGroup.Handle("/stats", onStats)
The built-in middleware.Whitelist does the same thing but silently skips the update instead of sending a reply. Write your own when you need a custom rejection response.

Logging middleware

The built-in middleware.Logger logs the raw update JSON. Here’s a leaner version that logs command name and latency:
func TimingLogger(logger *log.Logger) tele.MiddlewareFunc {
    return func(next tele.HandlerFunc) tele.HandlerFunc {
        return func(c tele.Context) error {
            start := time.Now()
            err := next(c)
            logger.Printf(
                "update=%d sender=%d elapsed=%s err=%v",
                c.Update().ID,
                c.Sender().ID,
                time.Since(start),
                err,
            )
            return err
        }
    }
}

Error recovery middleware

The built-in middleware.Recover handles panics. Extend it to also send the user an apology message:
b.Use(middleware.Recover(func(err error, c tele.Context) {
    log.Printf("recovered panic: %v", err)
    // Notify user without surfacing internal details
    _ = c.Send("An unexpected error occurred. Our team has been notified.")
    // Report to external service
    sentry.CaptureException(err)
}))
Always register Recover before other middleware so it wraps the entire chain. If registered last, panics in earlier middleware won’t be caught.

Build docs developers (and LLMs) love