Handling Telegram Bot Rate Limits with Redis

Handling Telegram Bot Rate Limits with Redis

What You’ll Need

  • Hetzner VPS or Contabo VPS for hosting
  • DigitalOcean as an alternative cloud provider
  • n8n Cloud or self-hosted n8n instance
  • Redis server version 6.0 or higher
  • Node.js runtime version 18 or higher
  • A valid Telegram Bot API token from BotFather

Table of Contents

Understanding Telegram Bot Rate Limits

When building high volume Telegram bots, hitting API rate limits is an inevitable challenge. Telegram enforces precise thresholds to protect its infrastructure from spam and server overload. If your bot sends messages too quickly, Telegram responds with an HTTP 429 status code containing a retry_after parameter, which tells your application how many seconds it must sleep before issuing another request.

Telegram applies rate limits across multiple levels:

  1. Per Chat Limit: You cannot send more than 1 message per second to a single individual chat.
  2. Global Limit: A bot cannot send more than 30 messages per second across all users and chats combined.
  3. Group Chat Limit: Bots are restricted to roughly 20 messages per minute inside a single group or channel.

If you violate these boundaries repeatedly, Telegram extends your cooldown period, and sustained violations can lead to temporary or permanent bot bans. Handling these limits synchronously inside your application code usually leads to blocked threads, memory leaks, or lost messages.

To prevent these failure points, you need a centralized rate limiter that controls execution throughput before making HTTP requests. Capturing structured events during high velocity message distribution is crucial, so consider Configuring Vector for Centralized Log Aggregation to stream HTTP response codes and rate limit spikes to a centralized dashboard.

Architecture: Sliding Window and Rate Limiting with Redis

A standard fixed-window rate limiter resets its counters at arbitrary time boundaries (such as every minute at 00 seconds). This approach creates execution bursts at the window edges, allowing up to double the allowed request volume in a narrow time frame.

To guarantee compliance with Telegram rules, we implement a Sliding Window Log algorithm backed by Redis sorted sets (ZSET). Every outbound message request generates a unique member in a sorted set where the score is a microsecond timestamp.

The process follows four atomic steps:

  1. Remove all entries in the sorted set older than the current timestamp minus the sliding window duration.
  2. Count the remaining elements in the set.
  3. If the count is below the permitted threshold, insert the new message timestamp into the set and proceed with dispatch.
  4. If the set is full, calculate the delay required until the oldest member expires, then reject or queue the outbound request.

Because Redis runs single threaded and processes operations sequentially, executing these commands atomically ensures race conditions are completely eliminated, even if you run multiple instances of your bot application in parallel.

Setting Up Docker and Redis Infrastructure

To build this setup on a Hetzner VPS or a DigitalOcean droplet, we deploy Redis and our Node.js bot service using Docker Compose.

Create a docker-compose.yml file in your application root directory:

version: '3.8'

services:
  redis:
    image: redis:7.2-alpine
    container_name: telegram_redis
    restart: always
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    command: redis-server --appendonly yes --requirepass SuperSecretRedisPassword123!
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "SuperSecretRedisPassword123!", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5

  bot:
    build: .
    container_name: telegram_bot_worker
    restart: always
    depends_on:
      redis:
        condition: service_healthy
    environment:
      - REDIS_HOST=redis
      - REDIS_PORT=6379
      - REDIS_PASSWORD=SuperSecretRedisPassword123!
      - TELEGRAM_BOT_TOKEN=123456789:ABCdefGHIjklMNOpqrsTUVwxyZ

volumes:
  redis_data:

When deploying containerized infrastructure, ensuring seamless service restarts without manual intervention is critical. Check out our guide on How to Deploy Watchtower for Docker Containers to automatically update your bot and Redis services whenever you push updates.

💡 Fast-Track Your Project: Don’t want to configure this yourself? I build custom n8n pipelines and bots. Message me with code SYS3-HUGO.

Implementing the Redis Rate Limiter in Node.js

Now we will write a production ready Node.js implementation using ioredis. This implementation runs an atomic Lua script directly inside Redis to ensure zero latency race conditions across workers.

First, initialize your application dependencies in package.json:

{
  "name": "telegram-rate-limiter",
  "version": "1.0.0",
  "description": "Telegram Bot Rate Limiting with Redis",
  "main": "index.js",
  "dependencies": {
    "axios": "^1.6.8",
    "bullmq": "^5.7.8",
    "dotenv": "^16.4.5",
    "ioredis": "^5.3.2"
  }
}

Next, create the core rate limiter file RateLimiter.js. This module uses an inline Lua script to run atomic rate limit validation against Redis keys:

const Redis = require('ioredis');

class RedisRateLimiter {
  constructor(redisClient) {
    this.redis = redisClient;
    this.defineLuaScript();
  }

  defineLuaScript() {
    this.luaScript = `
      local key = KEYS[1]
      local now = tonumber(ARGV[1])
      local window = tonumber(ARGV[2])
      local limit = tonumber(ARGV[3])
      local clearBefore = now - window

      redis.call('ZREMRANGEBYSCORE', key, 0, clearBefore)
      local currentRequests = redis.call('ZCARD', key)

      if currentRequests < limit then
        redis.call('ZADD', key, now, now)
        redis.call('PEXPIRE', key, window)
        return 1
      else
        return 0
      end
    `;

    this.redis.defineCommand('checkAndRecordLimit', {
      numberOfKeys: 1,
      lua: this.luaScript
    });
  }

  async isAllowed(targetKey, limit, windowMs) {
    const now = Date.now();
    const result = await this.redis.checkAndRecordLimit(targetKey, now, windowMs, limit);
    return result === 1;
  }
}

module.exports = RedisRateLimiter;

Building a High-Throughput Queue with BullMQ

If you attempt to send a message and hit a rate limit, dropping the payload is not an option. Instead, we insert the outgoing Telegram payloads into a persistent queue backed by BullMQ.

The architectural pattern matches the strategies described in How to Build Distributed Web Scraping Pipelines, where request rates must be dynamically modified without losing tasks from memory during traffic spikes.

Create QueueManager.js to manage queued message processing, rate limits, and Telegram API dispatches:

const { Queue, Worker } = require('bullmq');
const Redis = require('ioredis');
const axios = require('axios');
const RedisRateLimiter = require('./RateLimiter');

const connection = new Redis({
  host: process.env.REDIS_HOST || 'localhost',
  port: parseInt(process.env.REDIS_PORT || '6379'),
  password: process.env.REDIS_PASSWORD || 'SuperSecretRedisPassword123!',
  maxRetriesPerRequest: null
});

const rateLimiter = new RedisRateLimiter(connection);

const telegramQueue = new Queue('TelegramMessages', { connection });

const BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN;
const TELEGRAM_API_URL = `https://api.telegram.org/bot${BOT_TOKEN}/sendMessage`;

const sendTelegramMessage = async (chatId, text) => {
  return await axios.post(TELEGRAM_API_URL, {
    chat_id: chatId,
    text: text,
    parse_mode: 'HTML'
  });
};

const worker = new Worker('TelegramMessages', async (job) => {
  const { chatId, text } = job.data;

  const globalKey = 'ratelimit:telegram:global';
  const chatKey = `ratelimit:telegram:chat:${chatId}`;

  const globalAllowed = await rateLimiter.isAllowed(globalKey, 30, 1000);
  if (!globalAllowed) {
    throw new Error('RATE_LIMIT_GLOBAL_EXCEEDED');
  }

  const chatAllowed = await rateLimiter.isAllowed(chatKey, 1, 1000);
  if (!chatAllowed) {
    throw new Error('RATE_LIMIT_CHAT_EXCEEDED');
  }

  try {
    const response = await sendTelegramMessage(chatId, text);
    return response.data;
  } catch (error) {
    if (error.response && error.response.status === 429) {
      const retryAfterSeconds = error.response.data.parameters?.retry_after || 5;
      const delayMs = retryAfterSeconds * 1000;
      
      console.warn(`Hit 429 from Telegram API. Delaying chat ${chatId} for ${retryAfterSeconds}s`);
      
      await telegramQueue.add('sendMessage', job.data, {
        delay: delayMs,
        jobId: `retry_${job.id}_${Date.now()}`
      });

      return { delayed: true, retryAfterSeconds };
    }
    
    throw error;
  }
}, {
  connection,
  concurrency: 5,
  settings: {
    backoffStrategies: {
      customRateLimitBackoff: (attemptsMade) => {
        return attemptsMade * 1200;
      }
    }
  }
});

worker.on('failed', (job, err) => {
  if (err.message.startsWith('RATE_LIMIT_')) {
    telegramQueue.add('sendMessage', job.data, {
      delay: 1000,
      jobId: `rate_limit_retry_${job.id}_${Date.now()}`
    });
  } else {
    console.error(`Job ${job.id} failed permanently with error: ${err.message}`);
  }
});

const enqueueMessage = async (chatId, text) => {
  await telegramQueue.add('sendMessage', { chatId, text }, {
    attempts: 3,
    backoff: {
      type: 'customRateLimitBackoff'
    },
    removeOnComplete: true,
    removeOnFail: false
  });
};

module.exports = {
  enqueueMessage,
  telegramQueue,
  worker
};

To run and verify the system, create an index.js entry point that pushes a batch of test messages into the pipeline:

require('dotenv').config();
const { enqueueMessage } = require('./QueueManager');

const runTestBroadcast = async () => {
  const targetChatId = '987654321';
  console.log('Starting high-volume message test dispatch...');

  for (let i = 1; i <= 50; i++) {
    const messageText = `Test notification #${i} sent at timestamp ${new Date().toISOString()}`;
    await enqueueMessage(targetChatId, messageText);
    console.log(`Successfully queued message payload index: ${i}`);
  }
};

runTestBroadcast().catch((error) => {
  console.error('Error encountered while running message broadcast script:', error);
});

This dual layer architecture guarantees complete safety across all worker processes. The primary guard layer is the Redis sliding window script, which stops requests locally before they trigger network overhead.

The secondary guard layer handles any raw HTTP 429 responses sent by Telegram. When Telegram issues a rate limit response, the worker captures the exact retry_after window value, calculates the delay in milliseconds, and re-enqueues the job back into BullMQ.

By combining atomic sliding window evaluation in Redis with resilient BullMQ task queues, your bot maintains maximum execution speed without ever breaching Telegram’s system rate limits.

Getting Started

To launch your production ready rate limiting infrastructure:

  1. Provision a high performance server using Hetzner VPS or Contabo VPS.
  2. Set up DigitalOcean managed databases if you prefer external Redis hosting.
  3. Deploy your automation nodes using n8n Cloud or self-host your application services with Docker Compose.

Outsource Your Automation

Don’t have time? I build production n8n workflows, WhatsApp bots, and fully automated YouTube Shorts pipelines. Hire me on Fiverr, mention SYS3-HUGO for priority. Or DM at chasebot.online.

Want to automate this yourself?

Start with n8n Cloud (free tier available) or self-host on a Hetzner VPS for full control.

Want this engine running on your own VPS?

This blog publishes itself — daily, unattended, on free API tiers. The full engine, Hugo theme, and setup guide are available as System 3.

Get System 3
system online