Skip to main content
BuilderBot uses a sophisticated queue system to ensure messages are sent in the correct order and to prevent rate limiting from WhatsApp. Understanding the queue helps you build more reliable bots.

How the Queue Works

Every message sent by the bot goes through a queue system that:
  1. Maintains order - Messages are sent in the sequence they were queued
  2. Prevents duplicates - The same message won’t be sent twice
  3. Handles timeouts - Messages that take too long are automatically handled
  4. Supports concurrency - Multiple messages can be processed simultaneously

Queue Architecture

The queue system maintains separate queues for each user:
// Internally, the queue looks like this:
{
  '1234567890': [message1, message2, message3],  // User 1's queue
  '0987654321': [message1, message2],            // User 2's queue
}
Each user (identified by phone number) has their own independent queue. This means messages to different users don’t interfere with each other.

Queue Configuration

Configure queue behavior when creating your bot:
const { httpServer } = await createBot(
    {
        flow: adapterFlow,
        provider: adapterProvider,
        database: adapterDB,
    },
    {
        queue: {
            timeout: 50000,        // Timeout in ms (default: 50000)
            concurrencyLimit: 15   // Max concurrent messages (default: 15)
        }
    }
)

Configuration Options

Maximum time (in milliseconds) to wait for a message to be sent before timing out.
queue: {
    timeout: 30000  // 30 seconds
}
Default: 50000 (50 seconds)When to adjust: Increase for slow networks or large media files. Decrease for faster failure detection.
Maximum number of messages that can be processed simultaneously for each user.
queue: {
    concurrencyLimit: 5  // Process up to 5 messages at once
}
Default: 15When to adjust: Increase for faster message delivery. Decrease to avoid rate limiting or if you experience sending issues.

Queue Lifecycle

1

Message enters queue

When a message is triggered, it’s added to the user’s queue:
const flow = addKeyword('hello')
    .addAnswer('Message 1')  // Queued
    .addAnswer('Message 2')  // Queued
    .addAnswer('Message 3')  // Queued
2

Queue processes messages

Messages are processed based on concurrency limit:
If concurrencyLimit = 3:
- Message 1, 2, 3 process simultaneously
- Message 4, 5, 6 wait
- As 1, 2, 3 complete, 4, 5, 6 start
3

Messages are sent

Each message is sent to WhatsApp and marked as complete.
4

Queue clears

When all messages are sent, the queue for that user becomes empty.

Queue Methods

Access queue methods in flow callbacks:

clearQueue

Clear all pending messages for a user:
const cancelFlow = addKeyword('cancel')
    .addAction(async (ctx, { queue, flowDynamic }) => {
        queue.clearQueue(ctx.from)
        await flowDynamic('All pending messages cancelled')
    })

Accessing Queue State

The queue object is available in callbacks:
const debugFlow = addKeyword('debug')
    .addAction(async (ctx, { queue, flowDynamic }) => {
        const pendingIds = queue.getIdsCallback(ctx.from)
        await flowDynamic(`Pending messages: ${pendingIds.length}`)
    })

Handling Queue Issues

Duplicate Prevention

The queue automatically prevents duplicate messages:
// Even if triggered multiple times, this message sends only once
const flow = addKeyword('test')
    .addAnswer('This message', { ref: 'unique-ref-123' })
Each message has a unique reference ID. If the same reference is queued twice, the duplicate is ignored.

Timeout Handling

Messages that exceed the timeout are automatically resolved:
// If this takes longer than queue.timeout, it will timeout
const slowFlow = addKeyword('slow')
    .addAction(async (ctx, { flowDynamic }) => {
        // Simulate slow operation
        await slowOperation()  // If > 50s, triggers timeout
        await flowDynamic('Done!')
    })

Queue in Different Scenarios

Sequential Messages

Messages are queued and sent in order:
const flow = addKeyword('story')
    .addAnswer('Chapter 1...', { delay: 1000 })
    .addAnswer('Chapter 2...', { delay: 1000 })
    .addAnswer('Chapter 3...', { delay: 1000 })
    .addAnswer('The End!')
Queue behavior:
Time 0s:  Queue: [Ch1, Ch2, Ch3, End]
Time 1s:  Send Ch1, Queue: [Ch2, Ch3, End]
Time 2s:  Send Ch2, Queue: [Ch3, End]
Time 3s:  Send Ch3, Queue: [End]
Time 3s:  Send End, Queue: []

Concurrent Processing

With concurrencyLimit: 15, up to 15 messages process simultaneously:
const catalogFlow = addKeyword('catalog')
    .addAction(async (ctx, { flowDynamic }) => {
        const products = await getProducts()  // Returns 20 products
        
        for (const product of products) {
            await flowDynamic([
                { body: product.name, media: product.image }
            ])
        }
    })
Queue behavior:
Time 0s:  Queue: [P1, P2, ..., P20]
Time 0s:  Processing: [P1, P2, ..., P15]  (15 concurrent)
Time 1s:  P1-P15 complete, start P16-P20
Time 2s:  All complete, Queue: []

Force Queue Mode

Force all messages through the queue even during capture:
const flow = addKeyword('test')
    .addAnswer(
        'Enter your name:',
        { capture: true },
        async (ctx, { sendFlow }) => {
            const messages = [
                { answer: `Hello ${ctx.body}!` },
                { answer: 'Message 2' },
                { answer: 'Message 3' }
            ]
            
            // Force through queue
            await sendFlow(messages, ctx.from, { forceQueue: true })
        }
    )

Queue Logs

The queue system creates detailed logs in queue.class.log:
# View queue logs
tail -f queue.class.log
Log entries show:
  • When messages enter the queue
  • When messages start processing
  • When messages complete
  • Any errors or timeouts
1234567890: QUEUE: ans_abc123
1234567890: EXECUTING: ans_abc123
1234567890: SUCCESS: ans_abc123

Best Practices

Don’t perform long operations inside message callbacks:
// BAD: Blocks the queue
.addAction(async (ctx, { flowDynamic }) => {
    const data = await slowDatabaseQuery()  // Takes 30 seconds
    await flowDynamic(data)
})

// GOOD: Process async, then respond
.addAction(async (ctx, { flowDynamic }) => {
    await flowDynamic('Processing...')
    processAsync(ctx.from)  // Don't await
})
Adjust based on your use case:
// High-volume bot (many users)
queue: { concurrencyLimit: 5 }

// Low-volume bot (few users, fast responses)
queue: { concurrencyLimit: 20 }
Always clear the queue when ending conversations:
const endFlow = addKeyword('end')
    .addAction(async (ctx, { queue, endFlow }) => {
        queue.clearQueue(ctx.from)
        await endFlow('Conversation ended')
    })
Regularly check queue.class.log for issues:
# Watch for timeouts
grep "timeout" queue.class.log

# Check queue activity
tail -f queue.class.log
Large media files can cause timeouts. Increase timeout if needed:
queue: {
    timeout: 120000  // 2 minutes for large videos
}

Queue vs No Queue

Understanding when messages use the queue:
ScenarioUses QueueNotes
.addAnswer()YesAlways queued
flowDynamic()YesQueued by default
sendMessage() from APIYesUses queue
endFlow()YesClears queue after
Direct provider callsNoBypasses queue (not recommended)

Troubleshooting

This usually means you’re not properly awaiting async operations:
// BAD
.addAction(async (ctx, { flowDynamic }) => {
    flowDynamic('Message 1')  // Not awaited!
    flowDynamic('Message 2')  // Not awaited!
})

// GOOD
.addAction(async (ctx, { flowDynamic }) => {
    await flowDynamic('Message 1')
    await flowDynamic('Message 2')
})
Increase the timeout or optimize your operations:
queue: {
    timeout: 100000  // Increase to 100 seconds
}
Check logs and manually clear if needed:
// Emergency queue clear endpoint
adapterProvider.server.post('/v1/clear-queue',
    handleCtx(async (bot, req, res) => {
        const { number } = req.body
        bot.queue.clearQueue(number)
        res.end('cleared')
    })
)

Advanced: Queue Internals

For advanced users, understanding the queue structure:
class Queue<T> {
    private queue: Map<string, QueueItem<T>[]>        // User queues
    private timers: Map<string, NodeJS.Timeout>       // Timeouts
    private workingOnPromise: Map<string, boolean>    // Processing state
    
    async enqueue(from: string, promiseFunc: () => Promise<T>, id: string)
    async processQueue(from: string)
    async clearQueue(from: string)
}

Next Steps

Build docs developers (and LLMs) love