Ruby Telegram bot for concurrent users handling

I’m working on a Telegram bot using Ruby and need help with handling multiple users simultaneously. Currently my bot works fine with one person but breaks when several people try to use it at once.

I’m using the telegram-bot-ruby gem and building this as a standalone script without Rails. The bot needs to handle complex conversation flows for each user separately without their sessions interfering with each other.

Here’s my current approach:

Telegram::Bot::Client.run(bot_token) do |client|

  client.listen do |msg|
    case msg.text
    when '/begin'
      client.api.send_message(chat_id: msg.chat.id, text: "Hi there! Please tell me your username:")

      client.listen do |reply1|
        @username = reply1.text
        break
      end

      client.api.send_message(chat_id: msg.chat.id, text: "Great #{@username}! What's your birth year?")

      client.listen do |reply2| 
        @birth_year = reply2.text
        break
      end

    when '/exit'
      client.api.send_message(chat_id: msg.chat.id, text: "Goodbye!")

    else
      client.api.send_message(chat_id: msg.chat.id, text: "Type /begin to start")
    end
  end
end

The problem is that when user A is in the middle of entering their username and user B sends /begin, everything gets mixed up. Do I need to implement async handling or is there a different pattern I should follow? Any guidance would be really helpful.

Your nested listen loops are the problem. When one user hits the nested listener, it blocks everyone else until that conversation finishes. I ran into this same issue building a survey bot last year. Here’s what fixed it for me: ditch the nested listeners and use a state machine with a global hash to track where each user is. Handle everything in your main listen block and check the user’s current state. Set up user_states = {} at the top, then in your main handler do current_state = user_states[msg.chat.id] || 'idle' and route from there. When you need the username, just set user_states[msg.chat.id] = 'awaiting_username'. Next message from that chat_id gets processed as username input. Each user keeps their own conversation thread without blocking anyone else. The telegram-bot-ruby gem handles concurrency just fine once you drop those blocking loops.

Yeah, those nested client.listen blocks are creating a blocking mess. Managing user states manually with hashes gets messy quick once your conversation flows get complex.

I’ve built similar bots and trust me - handling state management, concurrent users, and complex workflows in one Ruby script becomes a maintenance nightmare. You’ll write tons of boilerplate just to track user sessions.

What worked way better for me was moving the conversation logic to an automation platform that handles state management automatically. I rebuilt a similar bot using Latenode and it was much cleaner. You set up the Telegram webhook, define conversation flows visually, and it handles multiple users without session mixing.

The platform stores user context automatically between steps. User A can enter their username while user B provides their birth year - no conflicts. You get built-in error handling and can modify conversation flows without touching code.

For complex bots with multiple conversation paths, this saves tons of dev time vs manual state management in Ruby.

The Problem:

You’re experiencing concurrency issues in your Telegram bot due to nested client.listen blocks in your Ruby code. This prevents your bot from handling multiple users simultaneously; when one user interacts with the bot, subsequent users are blocked until the first user’s conversation concludes. The core issue is not a limitation of the telegram-bot-ruby gem itself but rather the synchronous, blocking nature of your code’s design.

:thinking: Understanding the “Why” (The Root Cause):

The telegram-bot-ruby gem’s client.listen method operates on a single event loop. Nesting client.listen blocks forces sequential execution: the inner block only runs after the outer block completes processing a message. This creates a significant bottleneck, preventing concurrent handling of multiple users’ messages. Telegram delivers messages asynchronously, but your nested listen calls introduce artificial synchronicity, effectively turning an asynchronous system into a synchronous one.

:gear: Step-by-Step Guide:

  1. Replace Nested client.listen with a Single, Asynchronous Message Handler: This is the crucial change. Instead of nested client.listen blocks, use a single client.listen block that handles all incoming messages. Within this single block, you’ll manage the state of each user’s conversation using a data structure (like a Hash). This eliminates the blocking behavior of nested loops.

  2. Implement a User State Machine using a Hash: Create a hash (e.g., user_sessions) to track each user’s conversational state. Use the msg.chat.id as the key for each user’s session. The value associated with each key will be a hash containing the user’s current step in the conversation (e.g., 'idle', 'awaiting_username', 'awaiting_birth_year', 'completed').

  3. Refactor Your Code to Handle Each Message Independently: Rewrite your code to process each incoming message within the single client.listen block. Check the user’s current state from user_sessions and respond accordingly. The bot will no longer block while waiting for responses from one user because it will immediately process the next message from another user.

    Here’s an example of how you might refactor your code:

require 'telegram/bot'

bot_token = 'YOUR_BOT_TOKEN' # Replace with your bot token
user_sessions = {}

Telegram::Bot::Client.run(bot_token) do |client|
  client.listen do |msg|
    chat_id = msg.chat.id
    user_sessions[chat_id] ||= { step: 'idle' } # Initialize user session if it doesn't exist

    case user_sessions[chat_id][:step]
    when 'idle'
      if msg.text == '/begin'
        client.api.send_message(chat_id: chat_id, text: "Hi there! Please tell me your username:")
        user_sessions[chat_id][:step] = 'awaiting_username'
      else
        client.api.send_message(chat_id: chat_id, text: "Type /begin to start")
      end
    when 'awaiting_username'
      user_sessions[chat_id][:username] = msg.text
      client.api.send_message(chat_id: chat_id, text: "Great #{msg.text}! What's your birth year?")
      user_sessions[chat_id][:step] = 'awaiting_birth_year'
    when 'awaiting_birth_year'
      user_sessions[chat_id][:birth_year] = msg.text
      client.api.send_message(chat_id: chat_id, text: "Thank you!")
      user_sessions[chat_id][:step] = 'completed'
    when 'completed'
      # Add any actions for completed conversations here.
    end
  end
end
  1. Add /exit Command Handling: Implement a command (e.g., /exit) to reset a user’s session state in user_sessions, allowing them to start a new conversation.

  2. Thorough Testing: Test your refactored bot with multiple users concurrently to confirm that conversations no longer interfere with each other.

:mag: Common Pitfalls & What to Check Next:

  • Error Handling: Implement robust error handling (e.g., begin...rescue blocks) to gracefully handle unexpected user input or API errors.
  • Data Persistence: For persistent user session data beyond the bot’s runtime, consider a database or other persistent storage.
  • Scalability: For a large user base, investigate advanced concurrency and scaling techniques.

:speech_balloon: Still running into issues? Share your (sanitized) config files, the exact command you ran, and any other relevant details. The community is here to help!

Had this exact problem building a customer service bot last month. Your nested listen blocks are the issue - they create sequential execution where one user blocks everyone else until they finish. You need to handle each message independently. Set up a user state tracker outside your main listen loop and process messages based on where each user is in the conversation. Ditch the nested loops waiting for specific responses. Instead, store what each user needs to provide next and handle it when their message comes in. The telegram-bot-ruby gem handles concurrent connections fine - your conversation flow design is the bottleneck. Process each incoming message immediately without waiting for follow-ups. User A can enter their username while user B gives their birth year - no interference. Use a simple state machine where each chat_id has a state like ‘awaiting_username’ or ‘awaiting_birth_year’. Your main message handler checks the user’s current state and responds accordingly.

Just throw everything in the main listen block and use chat_id keys to track where each user is. I did the same thing - nested listens completely block other users. Set up a hash like sessions = {} and store each user’s step there instead of instance variables.

your nested client.listen blocks are causing this - they can’t isolate user sessions. store user states in a hash with chat_id as the key instead of using globals. try user_sessions[msg.chat.id][:step] = 'waiting_username' and handle responses based on each user’s current step.

This topic was automatically closed 24 hours after the last reply. New replies are no longer allowed.