Skip to content

Telegram request throttling per chat ID#147

Merged
orangecoding merged 5 commits intoorangecoding:masterfrom
alexanderroidl:feat/telegram-throttling-per-chat
Aug 1, 2025
Merged

Telegram request throttling per chat ID#147
orangecoding merged 5 commits intoorangecoding:masterfrom
alexanderroidl:feat/telegram-throttling-per-chat

Conversation

@alexanderroidl
Copy link
Contributor

Currently the Telegram Notification Adapter waits one second before sending each message:

/**
* This is to not break the rate limit. It is to only send 1 message per second
*/
return new Promise((resolve, reject) => {
setTimeout(() => {
fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
method: 'post',
body: JSON.stringify({
chat_id: chatId,
text: messageParagraphs.join('\n\n'),
parse_mode: 'HTML',
disable_web_page_preview: true,
}),
headers: { 'Content-Type': 'application/json' },
})
.then(() => {
resolve();
})
.catch(() => {
reject();
});
}, RATE_LIMIT_INTERVAL);
});

But as each timeout is started at the same time and all are awaited simultaneously...

return Promise.all(promises);

...they will still be sent together, just after a one second delay.

Telegram's FAQ says this about limits:

In a single chat, avoid sending more than one message per second. We may allow short bursts that go over this limit, but eventually you'll begin receiving 429 errors.

Which is why with this MR I implemented request throttling and set it to work per Chat ID, meaning each request will be sent immediately if there was none prior and only be waited for if another message to the same ID has been sent less than a second before.

@alexanderroidl alexanderroidl force-pushed the feat/telegram-throttling-per-chat branch from f3f8c30 to 210312b Compare July 26, 2025 07:18
const now = Date.now();
for (const [chatId, chatThrottle] of chatThrottleMap.entries()) {
if (now - chatThrottle.lastUsedAt > RATE_LIMIT_INTERVAL) {
chatThrottleMap.delete(chatId);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cleanupOldThrottles function iterates over chatThrottleMap.entries() and calls chatThrottleMap.delete(chatId) during iteration. In js modifying a Map (e.g., deleting entries) while iterating over it with entries() can lead to unpredictable behavior, such as skipped entries or runtime errors, especially if chatThrottleMap is accessed concurrently by multiple send calls.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might be able to mittigate the above effects with with increasing the rate_limit_interval to e.g. 10 secs and modifying the function like so:

function cleanupOldThrottles() {
  const now = Date.now();
  const keysToDelete = [];
  for (const [chatId, chatThrottle] of chatThrottleMap.entries()) {
    if (now - chatThrottle.lastUsedAt > RATE_LIMIT_INTERVAL) {
      keysToDelete.push(chatId);
    }
  }
  for (const chatId of keysToDelete) {
    chatThrottleMap.delete(chatId);
  }
}

WDTY?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch!! Love the idea and changed the code accordingly.

const RATE_LIMIT_INTERVAL = 1000;
const chatThrottleMap = new Map();

function cleanupOldThrottles() {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cleanupOldThrottles function deletes entries from chatThrottleMap if now - chatThrottle.lastUsedAt > RATE_LIMIT_INTERVAL. Since RATE_LIMIT_INTERVAL is 1 sec, entries are removed if they haven't been used in the last second. However, I think this is too aggressive for a rate-limiting system, as it could delete a throttle entry while it's still actively limiting requests. For example, if a chat sends a message at t=0ms and another at t=500ms, the throttle entry might be deleted at t=1001ms during a subsequent call, even though the throttle is still needed to enforce the rate limit for ongoing messages.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great point actually! I added an extra second, do you think this will be sufficient? :)

@orangecoding orangecoding merged commit b66f873 into orangecoding:master Aug 1, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants