<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Conferbot Engineering]]></title><description><![CDATA[Conferbot Engineering]]></description><link>https://conferbot.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6add850205e3eed42a66fe44/0c864710-8914-4564-b763-57d7f542efe3.png</url><title>Conferbot Engineering</title><link>https://conferbot.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Thu, 29 Oct 2026 19:25:56 GMT</lastBuildDate><atom:link href="https://conferbot.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Telegram Bot API Error Codes: What Each One Means and How to Fix It]]></title><description><![CDATA[Telegram Bot API error codes explained: 409 Conflict, 400 Bad Request, 403 Forbidden, 429 Too Many Requests, 401 Unauthorized. Literal errors, exact f]]></description><link>https://conferbot.hashnode.dev/telegram-bot-api-error-codes-what-each-one-means-and-how-to-fix-it</link><guid isPermaLink="true">https://conferbot.hashnode.dev/telegram-bot-api-error-codes-what-each-one-means-and-how-to-fix-it</guid><category><![CDATA[telegram]]></category><category><![CDATA[api]]></category><category><![CDATA[bots]]></category><category><![CDATA[webdev]]></category><dc:creator><![CDATA[Conferbot]]></dc:creator><pubDate>Sun, 25 Oct 2026 05:10:18 GMT</pubDate><content:encoded><![CDATA[<p><em>Originally published on <a href="https://www.conferbot.com/blog/telegram-bot-api-error-codes">conferbot.com</a>.</em></p>
<h2>How to read a Telegram Bot API error before you look it up</h2>
<p><strong>Telegram's HTTP status codes are coarse - almost everything is a 400 - but the <code>description</code> string is precise and documented. Read the description, not the code, and you will usually know the fix before you finish the sentence.</strong></p>
<p><em>Last verified: 2026-08-20 against the Telegram Bot API. Error strings quoted below are the literal responses the API returns.</em></p>
<p>A failed call always has the same shape:</p>
<pre><code>{
  "ok": false,
  "error_code": 429,
  "description": "Too Many Requests: retry after 34",
  "parameters": {"retry_after": 34}
}
</code></pre>
<p>Three fields matter. <code>error_code</code> mirrors the HTTP status. <code>description</code> is the real error - a stable, documented English string, usually prefixed with the status name (<code>Bad Request:</code>, <code>Forbidden:</code>, <code>Conflict:</code>). And the optional <code>parameters</code> object carries machine-readable follow-ups: <code>retry_after</code> (seconds to wait) and <code>migrate_to_chat_id</code> (the group became a supergroup; here is its new ID). Code that logs only the status code throws away everything useful.</p>
<p>The five families, by symptom:</p>
<table>
<thead>
<tr>
<th>Code</th>
<th>Family</th>
<th>Typical symptom</th>
</tr>
</thead>
<tbody><tr>
<td>409</td>
<td>Update-delivery conflicts</td>
<td>Bot is completely or intermittently silent</td>
</tr>
<tr>
<td>400</td>
<td>Bad Request - your payload</td>
<td>Specific sends fail; the rest work</td>
</tr>
<tr>
<td>403</td>
<td>Forbidden - the recipient</td>
<td>Sends to certain users or chats fail permanently</td>
</tr>
<tr>
<td>429</td>
<td>Flood control</td>
<td>Worked in testing, drops messages at volume</td>
</tr>
<tr>
<td>401 / 404</td>
<td>Token problems</td>
<td>Every single call fails</td>
</tr>
</tbody></table>
<p>This guide walks each family code-first. If your bot is silent and you do not have an error string yet, start instead with our symptom-first companion, <a href="https://www.conferbot.com/blog/telegram-bot-not-responding">Telegram bot not responding</a> - and for any literal string, the <a href="https://www.conferbot.com/errors/telegram">Telegram error directory</a> has a dedicated page per error.</p>
<h2>409 Conflict: the errors that make a bot silent</h2>
<p>Telegram permits exactly one consumer of updates per bot token - you either poll with <code>getUpdates</code> or receive pushes at a webhook, never both, and never two pollers. Both violations return 409, and both silence your bot while every other part of the system looks healthy.</p>
<pre><code>{"ok":false,"error_code":409,"description":"Conflict: can't use getUpdates method while webhook is active; use deleteWebhook to delete the webhook first"}
</code></pre>
<p>A webhook is registered against the token and your code is polling. Many libraries swallow this error and retry forever, so the process runs, the logs look fine, and nothing ever arrives. Fix: <code>deleteWebhook?drop_pending_updates=true</code>, then restart polling - or stop polling if the webhook was intentional. Full handling on the <a href="https://www.conferbot.com/errors/telegram/409-conflict-cant-use-getupdates-method-while-webhook-is-active">webhook-active 409 page</a>.</p>
<pre><code>{"ok":false,"error_code":409,"description":"Conflict: terminated by other getUpdates request; make sure that only one bot instance is running"}
</code></pre>
<p>Two processes are polling the same token, and each message goes to whichever holds the connection - the classic bot that answers roughly half the time. Hunt for a dev process left running, a container scaled to two replicas, or an old deploy that never died. Polling does not scale horizontally; if you need more than one instance, use webhooks behind a load balancer. Details on the <a href="https://www.conferbot.com/errors/telegram/409-conflict-terminated-by-other-getupdates-request">terminated-by-other-getUpdates page</a>.</p>
<p>Both 409s are configuration states, not transient faults: retrying without changing anything reproduces them forever. The one-command diagnostic is <code>getWebhookInfo</code> - an empty <code>url</code> means polling mode, a populated one means webhook mode, and a URL you do not recognise means a stale deploy is stealing your updates.</p>
<h2>400 Bad Request: sends that fail because of the message itself</h2>
<p>The 400 family is the largest, and it splits by what you got wrong. First, the message content errors - these are deterministic, so the bot works for most messages and dies on specific ones.</p>
<pre><code>{"ok":false,"error_code":400,"description":"Bad Request: can't parse entities: Can't find end of the entity starting at byte offset 42"}
</code></pre>
<p>You set <code>parse_mode</code> and the text contains characters the parser reads as markup. MarkdownV2 requires escaping <code>_ * [ ] ( ) ~ ` &gt; # + - = | { } . !</code> even in prose - a sentence ending in a full stop can fail. Escape interpolated user content, or switch to HTML parse mode, which needs only <code>&amp;</code>, <code>&lt;</code>, <code>&gt;</code>. The byte offset in the description points at the exact culprit; the <a href="https://www.conferbot.com/errors/telegram/400-bad-request-cant-parse-entities">can't parse entities page</a> has escaping tables for both modes.</p>
<pre><code>Bad Request: message is too long
Bad Request: message text is empty
</code></pre>
<p>Text messages cap at 4,096 characters - AI-generated replies overrun this constantly, so split on a paragraph boundary before sending (<a href="https://www.conferbot.com/errors/telegram/400-bad-request-message-is-too-long">message is too long</a>). The empty variant usually means an interpolation produced <code>""</code> or a template variable never resolved (<a href="https://www.conferbot.com/errors/telegram/400-bad-request-message-text-is-empty">message text is empty</a>).</p>
<p>Then the addressing errors:</p>
<pre><code>Bad Request: chat not found
Bad Request: user not found
Bad Request: chat_id is empty
</code></pre>
<p><a href="https://www.conferbot.com/errors/telegram/400-bad-request-chat-not-found">chat not found</a> means the <code>chat_id</code> does not correspond to a chat your bot can reach: a username where a numeric ID is needed, a dropped negative sign (group IDs are negative, supergroups start with <code>-100</code>), a chat ID truncated by a 32-bit integer column, or a group the bot was removed from. A user ID your bot has never seen in an update gives <a href="https://www.conferbot.com/errors/telegram/400-bad-request-user-not-found">user not found</a> - Telegram bots cannot look up arbitrary users; they can only address chats they have met.</p>
<h2>400 Bad Request: editing, deleting, and replying to messages</h2>
<p>A second cluster fires when you manipulate existing messages. Each description names its own rule:</p>
<table>
<thead>
<tr>
<th>Literal description</th>
<th>The rule you hit</th>
</tr>
</thead>
<tbody><tr>
<td><a href="https://www.conferbot.com/errors/telegram/400-bad-request-message-is-not-modified">message is not modified</a></td>
<td>Your edit produced identical content - common in loops that re-render unchanged state. Compare before editing.</td>
</tr>
<tr>
<td><a href="https://www.conferbot.com/errors/telegram/400-bad-request-message-cant-be-edited">message can't be edited</a></td>
<td>Too old, or not sent by your bot. Bots can only edit their own messages, within Telegram's edit window.</td>
</tr>
<tr>
<td><a href="https://www.conferbot.com/errors/telegram/400-bad-request-message-to-delete-not-found">message to delete not found</a></td>
<td>Already deleted, or the ID belongs to a different chat. Message IDs are per-chat, not global.</td>
</tr>
<tr>
<td><a href="https://www.conferbot.com/errors/telegram/400-bad-request-reply-message-not-found">message to be replied not found</a></td>
<td>The <code>reply_to_message_id</code> points at a deleted or foreign message. Send without the reply parameter as a fallback.</td>
</tr>
<tr>
<td><a href="https://www.conferbot.com/errors/telegram/400-bad-request-query-is-too-old-and-response-timeout-expired-or-query-id-is-invalid">query is too old and response timeout expired or query ID is invalid</a></td>
<td>You answered a callback query too late. <code>answerCallbackQuery</code> must be called promptly - answer first, process after, or the user's button spins forever.</td>
</tr>
</tbody></table>
<p>The callback-query row deserves emphasis because its symptom is so misleading: users report "the buttons stopped working", but the bot is receiving every tap - it is just answering after the query expired. The same acknowledge-first pattern that keeps webhooks healthy applies here: answer the callback immediately, then do the slow work.</p>
<h2>400 Bad Request: group permissions and the supergroup migration</h2>
<p>Group-related 400s look like bugs and are actually group administration:</p>
<pre><code>Bad Request: have no rights to send a message
Bad Request: not enough rights to send text messages to the chat
Bad Request: CHAT_WRITE_FORBIDDEN
</code></pre>
<p>All three mean the bot is in the chat but muted: restricted by an admin, stripped of send permissions, or blocked from writing in that channel or topic. The fix is in the group's member settings, not in code - promote the bot or lift the restriction (<a href="https://www.conferbot.com/errors/telegram/400-bad-request-have-no-rights-to-send-a-message">have no rights to send a message</a>, <a href="https://www.conferbot.com/errors/telegram/400-bad-request-chat-write-forbidden">CHAT_WRITE_FORBIDDEN</a>).</p>
<p>The one that breaks bots permanently:</p>
<pre><code>{"ok":false,"error_code":400,"description":"Bad Request: group chat was upgraded to a supergroup chat","parameters":{"migrate_to_chat_id":-1001234567890}}
</code></pre>
<p>When a group upgrades to a supergroup its chat ID changes forever, and the old ID never works again. Telegram hands you the new ID in <code>parameters.migrate_to_chat_id</code> - store it the moment you see it, and also handle the <code>migrate_to_chat_id</code> field that arrives in updates when the migration happens. Bots that ignore this silently lose the group; the <a href="https://www.conferbot.com/errors/telegram/400-bad-request-group-chat-was-upgraded-to-a-supergroup-chat">supergroup migration page</a> has the handler pattern.</p>
<h2>400 Bad Request: webhook registration refused</h2>
<p>These fire at <code>setWebhook</code> time, which is good news - the failure is loud and immediate instead of silent:</p>
<pre><code>Bad Request: bad webhook: HTTPS url must be provided for webhook
Bad Request: bad webhook: Webhook can be set up only on ports 80, 88, 443 or 8443
Bad Request: bad webhook: failed to resolve host
</code></pre>
<p>The rules behind them: webhooks are HTTPS-only with no development exception; only four ports are allowed, so the dev server on port 3000 can never receive a webhook (<a href="https://www.conferbot.com/errors/telegram/400-bad-request-bad-webhook-webhook-can-be-set-up-only-on-ports-80-88-443-or-8443">the ports 80, 88, 443, 8443 page</a> covers the workarounds - a reverse proxy or a tunnel); and the hostname must resolve on the public internet, so <code>localhost</code> and internal DNS names are rejected (<a href="https://www.conferbot.com/errors/telegram/400-bad-request-bad-webhook-failed-to-resolve-host">failed to resolve host</a>).</p>
<p>Once registration succeeds, failures move to delivery time and stop being errors you receive - they become entries in <code>getWebhookInfo</code>'s <code>last_error_message</code> field, which is where you look when a registered webhook goes quiet. That debugging path - certificates, timeouts, the acknowledge-then-process pattern - is shared across every platform, and our <a href="https://www.conferbot.com/blog/webhook-not-firing-debugging-guide">webhook debugging guide</a> walks it in one pass. The mechanics of what a <a href="https://www.conferbot.com/glossary/term/webhook">webhook</a> actually is, if the term is new, take two minutes to absorb.</p>
<h2>400 Bad Request: files, photos, and buttons</h2>
<p>Media and keyboard errors round out the 400 family:</p>
<table>
<thead>
<tr>
<th>Literal description</th>
<th>Cause and fix</th>
</tr>
</thead>
<tbody><tr>
<td><a href="https://www.conferbot.com/errors/telegram/400-bad-request-wrong-file-identifier-http-url-specified">wrong file identifier/HTTP URL specified</a></td>
<td>The <code>file_id</code> is malformed, from a different bot (file_ids are bot-scoped), or the URL is not a plain HTTPS file link.</td>
</tr>
<tr>
<td><a href="https://www.conferbot.com/errors/telegram/400-bad-request-file-is-too-big">file is too big</a></td>
<td>The cloud Bot API caps downloads at 20MB and uploads at 50MB. A self-hosted Bot API server raises both to roughly 2000MB.</td>
</tr>
<tr>
<td><a href="https://www.conferbot.com/errors/telegram/400-bad-request-photo-invalid-dimensions">PHOTO_INVALID_DIMENSIONS</a></td>
<td>Width/height ratio or size outside Telegram's photo rules - send extreme aspect ratios as a document instead.</td>
</tr>
<tr>
<td><a href="https://www.conferbot.com/errors/telegram/400-bad-request-failed-to-get-http-url-content">failed to get HTTP URL content</a></td>
<td>Telegram's servers could not fetch the URL you passed - dead link, auth-gated file, or a host blocking Telegram's fetchers. Host media somewhere publicly reachable.</td>
</tr>
<tr>
<td><a href="https://www.conferbot.com/errors/telegram/400-bad-request-button-url-invalid">BUTTON_URL_INVALID</a> / <a href="https://www.conferbot.com/errors/telegram/400-bad-request-button-data-invalid">BUTTON_DATA_INVALID</a></td>
<td>An inline keyboard button carries a malformed URL, or <code>callback_data</code> over the 64-byte cap. Use short opaque tokens in callback_data and look the payload up server-side.</td>
</tr>
</tbody></table>
<p>The 64-byte <code>callback_data</code> cap is the one that surprises people: serializing a JSON state object into the button works in testing and breaks the moment the state grows. Store state server-side, keyed by a short ID.</p>
<h2>403 Forbidden: the recipient is the problem, and retrying is pointless</h2>
<p>Every 403 means the same thing structurally: your request was valid, your token is fine, and the recipient is unreachable as a matter of policy. None of them are fixed by retrying - the correct response is always to update your database and stop sending.</p>
<pre><code>{"ok":false,"error_code":403,"description":"Forbidden: bot was blocked by the user"}
</code></pre>
<p>The user blocked your bot. There is no API to check block status in advance - the send attempt is the check. Mark the chat inactive; bots that keep retrying blocked users burn their own rate limit (<a href="https://www.conferbot.com/errors/telegram/403-forbidden-bot-was-blocked-by-the-user">bot was blocked by the user</a>).</p>
<pre><code>Forbidden: bot can't initiate conversation with a user
</code></pre>
<p>The rule every notification system meets on day one: <strong>a Telegram bot can never message someone first.</strong> The user must send <code>/start</code> or tap a deep link like <code>https://t.me/YourBotName?start=user_12345</code> before you may send anything. A phone number or user ID from another system grants nothing (<a href="https://www.conferbot.com/errors/telegram/403-forbidden-bot-cant-initiate-conversation-with-a-user">can't initiate conversation</a>).</p>
<p>The rest of the family, by literal string:</p>
<ul>
<li><a href="https://www.conferbot.com/errors/telegram/403-forbidden-bot-was-kicked-from-the-group-chat"><code>Forbidden: bot was kicked from the group chat</code></a> - removed by an admin; delete the stored chat and stop.</li>
<li><a href="https://www.conferbot.com/errors/telegram/403-forbidden-user-is-deactivated"><code>Forbidden: user is deactivated</code></a> - the account was deleted or deactivated; permanently unreachable.</li>
<li><a href="https://www.conferbot.com/errors/telegram/403-forbidden-bot-is-not-a-member-of-the-channel-chat"><code>Forbidden: bot is not a member of the channel chat</code></a> - to post to a channel the bot must be added as an administrator, not merely know the channel's ID.</li>
<li><a href="https://www.conferbot.com/errors/telegram/403-forbidden-bot-cant-send-messages-to-bots"><code>Forbidden: bot can't send messages to bots</code></a> - bots cannot talk to each other, by design. If two of your systems need to communicate, use your own backend, not Telegram.</li>
</ul>
<p>A well-behaved sender treats any 403 as a permanent state transition for that chat. Log it once, flag the record, move on.</p>
<h2>429 Too Many Requests: flood control and retry_after</h2>
<p>The bot that answered everything in testing and drops messages at launch is meeting flood control:</p>
<pre><code>{"ok":false,"error_code":429,"description":"Too Many Requests: retry after 34","parameters":{"retry_after":34}}
</code></pre>
<p>Telegram's published guidance for bots: roughly 30 messages per second overall, about 1 message per second sustained to an individual chat, and around 20 messages per minute to a single group. The full set of ceilings lives on the <a href="https://www.conferbot.com/limits/telegram">Telegram limits page</a>.</p>
<p>The contract when you exceed one: <strong>honour <code>retry_after</code> exactly.</strong> Retrying sooner extends the penalty, and sustained abuse can get a token limited far longer than the number suggests. The wrong fix is a sleep sprinkled into the send loop; the right fix is a token-bucket queue that paces broadcasts below 30 per second and spaces per-chat sends - the same architecture every platform ends up needing, as our <a href="https://www.conferbot.com/blog/chatbot-api-rate-limiting">chatbot API rate limiting guide</a> covers cross-platform. Back-off rules and code patterns are on the <a href="https://www.conferbot.com/errors/telegram/429-too-many-requests-retry-after">429 reference page</a>, and the general concept on the <a href="https://www.conferbot.com/glossary/term/rate-limiting">rate limiting</a> glossary page.</p>
<p>A subtler 429 trigger affects conversational bots: sending three quick bubbles to feel human - greeting, question, menu - can trip the per-chat limit for one user. Insert a short delay between bubbles; it also reads more naturally.</p>
<h2>401 Unauthorized and 404 Not Found: the token itself</h2>
<p>When <em>every</em> call fails, stop debugging payloads - the token is broken in one of two ways:</p>
<pre><code>{"ok":false,"error_code":401,"description":"Unauthorized"}
{"ok":false,"error_code":404,"description":"Not Found"}
</code></pre>
<p><a href="https://www.conferbot.com/errors/telegram/401-unauthorized">401 Unauthorized</a> means the token has a valid shape but does not authenticate: revoked via BotFather (regenerating a token instantly kills the old one), or corrupted in transit into your environment - a trailing newline from a secrets manager, quote characters included in the value, or a truncated paste. <a href="https://www.conferbot.com/errors/telegram/404-not-found">404 Not Found</a> usually means the token is malformed enough that the URL path itself is wrong - a missing <code>bot</code> prefix (<code>/bot&lt;TOKEN&gt;/getMe</code>), a typo'd method name, or an empty token variable producing <code>/bot/getMe</code>.</p>
<p>Verify the token in isolation before touching anything else: open <code>https://api.telegram.org/bot&lt;TOKEN&gt;/getMe</code> in a browser, or use our <a href="https://www.conferbot.com/tools/telegram-bot-token-checker">Telegram bot token checker</a>, which runs the same call from the browser and explains the response. A healthy token returns the bot's username and ID. If <code>getMe</code> succeeds locally but production returns 401, the token is fine and the deployed environment variable is not.</p>
<p>One trap with no error at all: a <em>valid but wrong</em> token - the staging bot's token deployed to production. Both tokens pass <code>getMe</code>, sends succeed, and users report silence because the replies are going out through a bot nobody is looking at. When logs show success and users see nothing, confirm which bot the token belongs to.</p>
<h2>502 and 504: when the failure is not yours</h2>
<p>Two gateway errors round out the list, and they matter mostly because of what you should <em>not</em> do about them:</p>
<ul>
<li><a href="https://www.conferbot.com/errors/telegram/502-bad-gateway"><code>502 Bad Gateway</code></a> - the Bot API server itself is having trouble, or a proxy between you and it is. Transient; retry with back-off.</li>
<li><a href="https://www.conferbot.com/errors/telegram/504-gateway-timeout"><code>504 Gateway Timeout</code></a> - the request did not complete in time. Also transient, but if it correlates with your own long-polling timeout settings, tune <code>getUpdates</code>' <code>timeout</code> parameter rather than hammering.</li>
</ul>
<p>The rule for 5xx is the inverse of the 403 rule: these are the only errors in this guide where blind retrying is correct, provided it backs off. Everything else in this guide is deterministic - a 400 will fail identically on retry, a 403 is a policy state, a 409 is configuration. A retry queue that cannot tell these classes apart will hammer permanent errors and give up on transient ones; classify on <code>error_code</code> first, then on the description string.</p>
<p>Distinguish carefully between a 502 <em>from</em> Telegram and a 502 <em>in your webhook's</em> <code>last_error_message</code> - the second means Telegram called your server and your server answered 502, which is your app crashing, not Telegram's. <code>getWebhookInfo</code> tells you which side you are on.</p>
<h2>Avoiding this layer altogether</h2>
<p>Every error family above is transport plumbing: update-consumer exclusivity, escaping rules, per-chat pacing, token hygiene, migration handling. None of it is your product, and all of it recurs on every platform under different names - Meta's error codes, Slack's, LINE's. Handling it well is genuinely necessary if you operate the Bot API directly, and genuinely undifferentiating work if what you actually want is a bot that answers customers.</p>
<p>A managed platform absorbs this layer. With <a href="https://www.conferbot.com/chatbot/telegram">Conferbot's Telegram integration</a>, you connect a bot token and build the conversation in a visual flow editor - webhook lifecycle, retry classification, rate-limit pacing, and message formatting are handled for you, and the same flow deploys to <a href="https://www.conferbot.com/chatbot/whatsapp">WhatsApp</a>, <a href="https://www.conferbot.com/chatbot/line">LINE</a>, and a <a href="https://www.conferbot.com/chatbot/website">website widget</a> without re-implementing each platform's error table.</p>
<p><a href="https://www.conferbot.com/free"><strong>Start free with Conferbot</strong></a> - no credit card required, so you can compare a managed flow against your hand-rolled handler this afternoon.</p>
<p>If you are staying hands-on: bookmark the <a href="https://www.conferbot.com/errors/telegram">Telegram error directory</a> for the string-level pages, keep the <a href="https://www.conferbot.com/limits/telegram">limits page</a> next to your queue configuration, and when the bot is silent with no error in sight, work through <a href="https://www.conferbot.com/blog/telegram-bot-not-responding">the symptom-first guide</a> - between the two, every failure in this family tree has a page.</p>
<h2>FAQ</h2>
<p><strong>What does error 409 mean in the Telegram Bot API?</strong></p>
<p>Conflict: two things are competing for the same bot token's updates. Either a webhook is registered while your code polls with getUpdates - fix with deleteWebhook - or two processes are polling simultaneously, which splits messages between them roughly at random. Telegram allows exactly one update consumer per token. Call getWebhookInfo to see which state you are in before changing any code.</p>
<p><strong>How do I fix "Bad Request: chat not found" on Telegram?</strong></p>
<p>The chat_id does not correspond to a chat your bot can reach. Check for a username used where a numeric ID is required, a dropped negative sign - group IDs are negative and supergroup IDs start with -100 - a value truncated by a 32-bit integer column, or a group that upgraded to a supergroup and changed its ID. Store chat IDs as 64-bit integers or strings and handle migrate_to_chat_id.</p>
<p><strong>What causes "Forbidden: bot was blocked by the user"?</strong></p>
<p>Exactly what it says: the user blocked your bot, and there is no API to check block status in advance - the send attempt is the check. Mark that chat inactive in your database and stop sending, because retries will never succeed and only consume your rate limit. Treat every 403 in the family the same way: a permanent state change for that recipient, not a transient fault.</p>
<p><strong>What is retry_after in a Telegram 429 error?</strong></p>
<p>The number of seconds Telegram requires you to wait before the next request, returned in the parameters object alongside the literal description "Too Many Requests: retry after N". Honour it exactly - retrying sooner extends the penalty. If you hit 429 regularly, the structural fix is a token-bucket queue pacing sends below roughly 30 messages per second overall and about one per second per chat.</p>
<p><strong>Why does my Telegram bot get 401 Unauthorized on every call?</strong></p>
<p>The token does not authenticate: it was regenerated in BotFather - which instantly invalidates the old value - or the environment variable is corrupted by a trailing newline, included quotes, or truncation. Test the token alone with getMe in a browser or a token checker tool. If getMe works locally but production fails, the token is fine and the deployed environment variable is what is broken.</p>
<p><strong>What is the difference between Telegram error 401 and 404?</strong></p>
<p>401 Unauthorized means the token is well-formed but invalid - revoked or corrupted. 404 Not Found usually means the request URL itself is wrong: a missing bot prefix before the token, a typo in the method name, or an empty token variable collapsing the path. Both make every call fail, so verify the exact URL string and the token value together before debugging anything else.</p>
<p><strong>How do I fix "can't parse entities" errors from Telegram?</strong></p>
<p>Your parse_mode treats part of the text as markup. MarkdownV2 demands escaping for sixteen characters including underscore, brackets, hyphen, and even the full stop, so unescaped user content fails on names and product codes. Escape everything you interpolate, or switch to HTML parse mode, which only needs ampersand and angle brackets escaped. The byte offset in the error points at the exact failing character.</p>
<p><strong>Why do sends to a group suddenly fail after it becomes a supergroup?</strong></p>
<p>Upgrading a group to a supergroup permanently changes its chat ID, and the old ID stops working forever. Telegram tells you twice: the failed send returns "group chat was upgraded to a supergroup chat" with the new ID in parameters.migrate_to_chat_id, and an update carrying migrate_to_chat_id arrives at migration time. Store the new ID the moment you see either signal.</p>
<p><strong>What are the file size limits for Telegram bots?</strong></p>
<p>On the default cloud Bot API server, bots can download files up to 20MB and upload files up to 50MB; exceeding either returns "Bad Request: file is too big". Running Telegram's self-hostable Bot API server raises both limits to roughly 2000MB and also lifts the webhook port restriction - worthwhile only when large media is a real requirement rather than a default choice.</p>
<p><strong>Why is BUTTON_DATA_INVALID returned for my inline keyboard?</strong></p>
<p>The callback_data on an inline button exceeds the 64-byte cap or contains invalid content. Serializing JSON state into the button is the usual cause - it fits during testing and overflows when the state grows. Store the state server-side keyed by a short opaque token, and put only that token in callback_data. The companion BUTTON_URL_INVALID means a button URL is malformed.</p>
<p><strong>Should I retry failed Telegram Bot API requests?</strong></p>
<p>Only by class. Retry 5xx errors with back-off, and 429 after exactly retry_after seconds. Never blind-retry 400s - they are deterministic and will fail identically - and never retry 403s, which are permanent recipient states like blocks and kicks. The 409s are configuration conflicts that persist until you change the setup. A retry queue that cannot classify by error_code hammers permanent errors and abandons transient ones.</p>
<p><strong>Where do webhook delivery errors appear if Telegram does not return them?</strong></p>
<p>In getWebhookInfo. Registration problems fail loudly at setWebhook time with a bad webhook description, but once a webhook is registered, delivery failures stop being API responses - Telegram records the latest one in the last_error_message field, with a timestamp in last_error_date and a pending_update_count showing the backlog. A populated last_error_message plus a climbing count means your endpoint is rejecting every delivery.</p>
<hr />
<p>The full guide, with diagrams and the latest checks, is at <a href="https://www.conferbot.com/blog/telegram-bot-api-error-codes">https://www.conferbot.com/blog/telegram-bot-api-error-codes</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Slack Bot Not Responding? Fix URL Verification, Missing Scopes and Silent Events]]></title><description><![CDATA[Slack bot not responding? Fix the url_verification challenge, missing_scope errors, events not firing, the 3-second deadline and duplicate retries. Ex]]></description><link>https://conferbot.hashnode.dev/slack-bot-not-responding-fix-url-verification-missing-scopes-and-silent-events</link><guid isPermaLink="true">https://conferbot.hashnode.dev/slack-bot-not-responding-fix-url-verification-missing-scopes-and-silent-events</guid><category><![CDATA[slack]]></category><category><![CDATA[bots]]></category><category><![CDATA[api]]></category><category><![CDATA[Tutorial]]></category><dc:creator><![CDATA[Conferbot]]></dc:creator><pubDate>Sun, 25 Oct 2026 05:08:54 GMT</pubDate><content:encoded><![CDATA[<p><em>Originally published on <a href="https://www.conferbot.com/blog/slack-bot-not-responding">conferbot.com</a>.</em></p>
<h2>Why Slack bots fail silently, and where to look first</h2>
<p><strong>Slack's failure modes are unusually quiet. A missing scope, an unverified request URL and a bot that was never invited to the channel all produce the same observable symptom: nothing happens. Your logs are empty because the request never arrived.</strong></p>
<p><em>Last verified: August 2026 against the <a href="https://docs.slack.dev/apis/events-api">Slack Events API</a> and <a href="https://docs.slack.dev/reference/scopes">OAuth scopes</a> model.</em></p>
<p>Before changing code, answer one question: <strong>is your <a href="https://www.conferbot.com/glossary/term/api-endpoint">endpoint</a> receiving anything at all?</strong> Add a log line as the very first statement in your handler, before any parsing or validation, and send a message that should trigger the bot.</p>
<table>
<thead>
<tr>
<th>Observation</th>
<th>Meaning</th>
<th>Section</th>
</tr>
</thead>
<tbody><tr>
<td>Nothing logged at all</td>
<td>Slack is not sending - subscription, verification or invite problem</td>
<td>1, 2, 4</td>
</tr>
<tr>
<td>Request logged, no reply appears</td>
<td>Missing scope, or the send is failing</td>
<td>3</td>
</tr>
<tr>
<td>Reply appears several times</td>
<td>Retry storm from a slow handler</td>
<td>5</td>
</tr>
<tr>
<td>Works in one channel only</td>
<td>Bot not invited elsewhere</td>
<td>4</td>
</tr>
</tbody></table>
<p>That single log line eliminates roughly half the possible causes in under a minute.</p>
<h2>1. URL verification: "Your URL didn't respond with the value of the challenge parameter"</h2>
<p>Before Slack will deliver any event, it verifies you own the endpoint. This is the same push shape as any incoming <a href="https://www.conferbot.com/glossary/term/webhook">webhook</a>: Slack POSTs a JSON body:</p>
<pre><code>{
  "token": "Jhj5dZrVaK7ZwHHjRyZWjbDl",
  "challenge": "3eZbrw1aB1m5zVFQrRvBiVAG1MMqbGqBGRTLBSFwhcSFrSKzHJ",
  "type": "url_verification"
}
</code></pre>
<p>You must respond with the <code>challenge</code> value and nothing else. This must happen <strong>before</strong> any signature verification, authentication middleware or body parsing that might reject an unfamiliar payload:</p>
<pre><code>app.post('/slack/events', (req, res) =&gt; {
  if (req.body.type === 'url_verification') {
    return res.send(req.body.challenge);   // plain text, first thing
  }
  // ... normal event handling below
});
</code></pre>
<h3>Five reasons this fails</h3>
<ul>
<li><strong>Signature verification runs first.</strong> Your middleware rejects the verification request because it does not match an expected shape. Handle <code>url_verification</code> before any guard.</li>
<li><strong>You returned JSON instead of the raw string.</strong> Returning <code>{"challenge": "..."}</code> also works if the content type is correct, but returning the plain string is more reliable.</li>
<li><strong>A redirect sits in front.</strong> Slack does not follow redirects during verification. If your URL 301s from HTTP to HTTPS or from a bare domain to www, register the final URL directly.</li>
<li><strong>Authentication is required.</strong> Any auth layer, IP allowlist or WAF rule in front of the endpoint blocks Slack. The events path must be publicly reachable.</li>
<li><strong>Cold start timeout.</strong> Slack allows about 3 seconds. A serverless function with a slow cold start fails verification, then succeeds when you retry immediately because the instance is warm - which makes this look intermittent.</li>
</ul>
<p>During local development, use a tunnel such as ngrok, and remember the URL changes every restart on the free tier - you must re-verify each time. If that becomes tedious, <a href="https://docs.slack.dev/apis/events-api/using-socket-mode"><strong>Socket Mode</strong></a> removes the public endpoint requirement entirely and is the better development path.</p>
<h2>2. Scopes: the reinstall step everyone forgets</h2>
<p>Slack's most common runtime error:</p>
<pre><code>{"ok": false, "error": "missing_scope", "needed": "chat:write", "provided": "channels:read,users:read"}
</code></pre>
<p>Helpfully, the response names exactly what you need. The trap is what comes next.</p>
<p><strong>Adding a scope in the app configuration does nothing until the app is reinstalled to the workspace.</strong> The token you already hold was issued with the old scope set and does not gain new permissions retroactively. Change scopes, then go to <strong>OAuth &amp; Permissions -&gt; Reinstall to Workspace</strong>, and use the newly issued token. Skipping the reinstall is the single most common Slack integration mistake.</p>
<h3>The scopes a conversational bot actually needs</h3>
<table>
<thead>
<tr>
<th>Scope</th>
<th>Grants</th>
</tr>
</thead>
<tbody><tr>
<td><code>chat:write</code></td>
<td>Send messages to channels the bot is in</td>
</tr>
<tr>
<td><a href="https://docs.slack.dev/reference/events/app_mention"><code>app_mentions:read</code></a></td>
<td>Receive @-mentions of the bot</td>
</tr>
<tr>
<td><code>im:history</code>, <code>im:write</code></td>
<td>Read and send direct messages</td>
</tr>
<tr>
<td><code>channels:history</code></td>
<td>Read messages in public channels the bot is in</td>
</tr>
<tr>
<td><code>groups:history</code></td>
<td>Read messages in private channels - separate from the above</td>
</tr>
<tr>
<td><code>users:read</code></td>
<td>Resolve user IDs to names</td>
</tr>
<tr>
<td><code>chat:write.public</code></td>
<td>Post to public channels without being invited</td>
</tr>
</tbody></table>
<p>Two distinctions matter. Public and private channel history are <strong>separate scopes</strong>, so a bot that reads public channels fine and sees nothing in private ones needs <code>groups:history</code>. And bot tokens (<code>xoxb-</code>) differ from user tokens (<code>xoxp-</code>): most bot actions need the bot token, and using the wrong one produces confusing permission errors.</p>
<h3>App manifests remove the drift between environments</h3>
<p>A meaningful share of scope mismatches come from configuring scopes by hand in one environment and forgetting to replicate the change in another. Slack's <a href="https://docs.slack.dev/app-manifests"><strong>App Manifest</strong></a> feature collapses the whole configuration - display information, bot scopes, event subscriptions, slash commands, interactivity settings and Socket Mode - into a single YAML or JSON document under the app's <strong>App Manifest</strong> tab. Treating that file as version-controlled configuration, rather than clicking through the dashboard per environment, means a scope added for staging cannot silently fail to reach production, and a manifest diff shows you exactly what changed before you reinstall. It will not save you from the reinstall step itself - a manifest update to <code>oauth_config.scopes</code> still requires reinstalling to take effect - but it does remove the class of bug where two environments quietly diverge.</p>
<h2>3. Events not firing at all</h2>
<p>If your endpoint verified successfully but no events arrive, work these in order.</p>
<h3>You subscribed to no events</h3>
<p>Verifying the URL and subscribing to events are separate steps. Under <strong>Event Subscriptions -&gt; Subscribe to bot events</strong>, add the specific events you need - most commonly <code>message.channels</code>, <code>message.im</code>, <code>message.groups</code> and <code>app_mention</code>. An empty subscription list means Slack has nothing to send.</p>
<h3>You subscribed to the wrong message event</h3>
<p>Message events are split by conversation type, and each is a distinct subscription:</p>
<ul>
<li><code>message.channels</code> - public channels</li>
<li><code>message.groups</code> - private channels</li>
<li><code>message.im</code> - direct messages</li>
<li><code>message.mpim</code> - group direct messages</li>
</ul>
<p>A bot that responds in public channels and ignores DMs is subscribed to <code>message.channels</code> but not <code>message.im</code>. This is the Slack equivalent of the intent split in other platforms.</p>
<h3>The bot is not in the channel</h3>
<p>Subscribing to <code>message.channels</code> does not deliver messages from channels the bot has not joined. Invite it with <code>/invite @yourbot</code> in each channel where it should work. A bot that works in one channel and not another has almost always just not been invited.</p>
<p>Attempting to post to a channel it is not in returns:</p>
<pre><code>{"ok": false, "error": "not_in_channel"}
</code></pre>
<p>The <code>chat:write.public</code> scope allows posting to public channels without an invite, but it does not grant the ability to <em>read</em> them - you still need the bot in the channel to receive messages.</p>
<h3>Your app is on the wrong delivery mode</h3>
<p>Socket Mode and the Events API are mutually exclusive. If Socket Mode is enabled, Slack pushes over a <a href="https://www.conferbot.com/glossary/term/websocket">WebSocket</a> and <strong>ignores your request URL entirely</strong> - so a perfectly correct HTTP endpoint receives nothing. Teams that enable Socket Mode for local development and forget to disable it before deploying hit this exactly once, and it is baffling until you know. Socket Mode has its own token requirement too: it needs a separate app-level token starting with <code>xapp-</code>, generated from the app's Basic Information page with the <code>connections:write</code> scope, which is easy to skip if you only ever set up a bot token before.</p>
<h2>4. The 3-second rule and the retry storm</h2>
<p><strong>Slack requires an HTTP 200 within 3 seconds of delivering an event.</strong> Miss it and Slack retries - up to three times, at roughly increasing intervals (the first retry lands almost immediately, the second after about a minute, the third after about five minutes). Your handler runs again each time, and the user receives the same reply three or four times.</p>
<p>This is the cause of nearly every "my Slack bot sends duplicates" report, and it becomes certain the moment you add an AI model call to the handler.</p>
<p>Acknowledge first, then work:</p>
<pre><code>app.post('/slack/events', async (req, res) =&gt; {
  res.sendStatus(200);              // acknowledge immediately, always
  await handleEvent(req.body);      // then take as long as you need
});
</code></pre>
<p>For slash commands and interactive components you can also respond immediately with a placeholder and update it later using the <code>response_url</code>, which stays valid for 30 minutes and accepts up to five follow-up messages.</p>
<h3>Detect retries rather than reprocessing them</h3>
<p>Slack marks retried deliveries with headers you should check:</p>
<pre><code>X-Slack-Retry-Num: 1
X-Slack-Retry-Reason: http_timeout
</code></pre>
<p><code>X-Slack-Retry-Reason</code> is worth logging on its own - besides <code>http_timeout</code> it can read <code>connection_failed</code>, <code>ssl_error</code>, <code>http_error</code> or <code>too_many_redirects</code>, and each points somewhere different: a timeout means your handler is too slow, but <code>ssl_error</code> or <code>too_many_redirects</code> means the retry never had a chance to reach your code regardless of how fast it responds. If <code>X-Slack-Retry-Num</code> is present at all, acknowledge and return without reprocessing. Combine that with deduplication on <code>event_id</code> and duplicate replies disappear entirely - even if a genuine timeout occurs.</p>
<p>The acknowledge-first pattern and the raw-body signature check are the same on every platform that calls you over HTTP; our <a href="https://www.conferbot.com/blog/webhook-not-firing-debugging-guide">cross-platform webhook debugging guide</a> shows the Node implementation once and maps it to Slack, Meta, Telegram and Discord.</p>
<h2>5. Infinite loops, thread targeting and ID mistakes</h2>
<h3>The bot replying to itself</h3>
<p>Bot messages generate message events too. Without a guard, your bot replies to its own reply indefinitely until Slack rate limits it. Check for the bot marker as the first line of the handler:</p>
<pre><code>if (event.bot_id || event.subtype === 'bot_message') return;
</code></pre>
<p>Two unguarded bots in one channel will do this to each other and produce thousands of messages in seconds.</p>
<h3>Message subtypes you probably want to ignore</h3>
<p>Edits, deletions, joins and file shares all arrive as message events with a <code>subtype</code>. A bot that responds to <code>message_changed</code> will reply again every time someone edits a message. Handle only the subtypes you intend to.</p>
<h3>Replying in the wrong place</h3>
<p>To reply inside a thread, pass <code>thread_ts</code> - the <code>ts</code> of the parent message. Omit it and your reply lands in the channel, out of context. Conversely, passing <code>thread_ts</code> when you meant to post to the channel buries the message in a thread nobody is watching.</p>
<h3>channel_not_found</h3>
<p>Usually a channel <em>name</em> used where an <em>ID</em> was required. Slack channel IDs look like <code>C01ABCDEF</code>; using <code>#general</code> works in some endpoints and not others. Store IDs rather than names - names change, IDs do not. The same error appears for private channels the bot has not been invited to, since it genuinely cannot see them.</p>
<h3>Rate limits</h3>
<p>Slack <a href="https://docs.slack.dev/apis/web-api/rate-limits">rate limits per method in tiers</a>, and <a href="https://docs.slack.dev/reference/methods/chat.postMessage"><code>chat.postMessage</code></a> is generally around one message per second per channel with short bursts tolerated. Exceeding it returns HTTP 429 with a <code>Retry-After</code> header in seconds - honour it exactly rather than retrying immediately. Web API methods beyond message-sending are grouped into four broader tiers, each method carrying its own per-minute ceiling per workspace; the tier for any given method is documented on its own reference page. A bot calling a Tier 1 method - the most restrictive tier - in a tight loop, such as polling <code>users.info</code> once per incoming message instead of caching the result, will get throttled long before it comes anywhere near the <code>chat.postMessage</code> limit. Caching lookups you would otherwise repeat on every event is the single highest-leverage fix for Slack rate limiting, well before backoff logic matters. For the equivalent ceilings on WhatsApp, Telegram and Discord, see our <a href="https://www.conferbot.com/limits">platform limits directory</a>.</p>
<h2>The diagnostic checklist</h2>
<table>
<thead>
<tr>
<th>#</th>
<th>Check</th>
<th>If it fails</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>Request URL shows Verified</td>
<td>Handle <code>url_verification</code> before any middleware</td>
</tr>
<tr>
<td>2</td>
<td>Socket Mode is OFF in production</td>
<td>It overrides your request URL completely</td>
</tr>
<tr>
<td>3</td>
<td>Bot events are subscribed, not just the URL verified</td>
<td>Add <code>message.im</code>, <code>message.channels</code>, <code>app_mention</code></td>
</tr>
<tr>
<td>4</td>
<td>App reinstalled since the last scope change</td>
<td>Old tokens never gain new scopes</td>
</tr>
<tr>
<td>5</td>
<td>Bot invited to the channel</td>
<td><code>/invite @yourbot</code></td>
</tr>
<tr>
<td>6</td>
<td>Handler returns 200 within 3 seconds</td>
<td>Acknowledge first, process after</td>
</tr>
<tr>
<td>7</td>
<td>Bot messages ignored</td>
<td>Guard on <code>bot_id</code> to prevent loops</td>
</tr>
<tr>
<td>8</td>
<td>Full API error bodies logged</td>
<td>Slack names the missing scope for you</td>
</tr>
</tbody></table>
<p>Row 8 deserves emphasis. Slack's <code>missing_scope</code> response includes both <code>needed</code> and <code>provided</code>, which is as close to a self-diagnosing error as any platform offers. Code that logs only a status code discards the answer. For the platforms that publish full numeric code sets, our <a href="https://www.conferbot.com/errors">error-code directory</a> maps each code to its fix.</p>
<h2>6. Buttons, modals and slash commands that do nothing</h2>
<p>Interactive components fail differently from events, and they fail more visibly - the user sees a spinner or an error in the Slack client rather than silence.</p>
<h3>The Interactivity request URL is separate</h3>
<p>This is the most common cause and the least obvious. <strong>Event Subscriptions and Interactivity &amp; Shortcuts have their own separate request URLs.</strong> Verifying your events endpoint does nothing for button clicks. If your bot posts a message with buttons and nothing happens when they are pressed, open <strong>Interactivity &amp; Shortcuts</strong>, turn interactivity on, and set a request URL there too - it can be the same endpoint, but it must be configured.</p>
<h3>Interaction payloads are form-encoded, not JSON</h3>
<p>Slack sends interactive payloads as <code>application/x-www-form-urlencoded</code> with a single <code>payload</code> field containing JSON as a string. An endpoint that only parses JSON receives nothing usable:</p>
<pre><code>app.post('/slack/interactive', (req, res) =&gt; {
  res.sendStatus(200);                          // acknowledge first
  const payload = JSON.parse(req.body.payload); // note: not req.body
  handleInteraction(payload);
});
</code></pre>
<p>A handler that works perfectly for events and throws on every button click is almost always missing this. Make sure your body parser handles URL-encoded bodies on that route.</p>
<h3>trigger_id expires in 3 seconds</h3>
<p>Opening a modal requires a <code>trigger_id</code> from the interaction payload, and it is valid for <strong>3 seconds only</strong>. You cannot defer, queue, or await an AI call before calling <code>views.open</code> - the trigger will be dead. Open the modal immediately with a loading state, then update it with <code>views.update</code> once the slow work finishes.</p>
<p>The error you get is <code>expired_trigger_id</code>, and it appears intermittently under load, which makes it look like a flaky network problem rather than a design constraint.</p>
<h3>Slash commands have their own URL too</h3>
<p>Each slash command is registered individually under <strong>Slash Commands</strong>, each with its own request URL. Commands also require the app to be reinstalled after being added. And a command name already taken by another app in that workspace will silently route to the other app - if your <code>/help</code> command triggers something unexpected, a competing app owns it.</p>
<h3>Block Kit validation failures</h3>
<p>Malformed Block Kit JSON returns <code>invalid_blocks</code> with little detail. Common causes are exceeding 50 blocks in a message, a text field over 3,000 characters, more than 5 buttons in an actions block, or a missing required field on an element. Validate structures in Slack's Block Kit Builder before shipping - it catches these instantly, and the API does not.</p>
<h2>7. Four tokens that look similar and do different jobs</h2>
<p>A share of "my bot is not responding" reports trace back to the right code holding the wrong kind of token. Slack issues several, and they are only distinguishable by their prefix:</p>
<table>
<thead>
<tr>
<th>Prefix</th>
<th>Type</th>
<th>Used for</th>
</tr>
</thead>
<tbody><tr>
<td><code>xoxb-</code></td>
<td>Bot token</td>
<td>The app's own identity. Almost everything in this guide - <code>chat.postMessage</code>, reading channel history - uses this one.</td>
</tr>
<tr>
<td><code>xoxp-</code></td>
<td>User token</td>
<td>Acts as a specific human who authorized it. Wrong token for almost any bot action - using it where <code>xoxb-</code> is expected produces confusing permission errors that look like a scope problem but are not.</td>
</tr>
<tr>
<td><code>xapp-</code></td>
<td>App-level token</td>
<td>Required specifically for <a href="#events-not-firing">Socket Mode</a>. Generated from Basic Information, and it needs the <code>connections:write</code> scope or the WebSocket connection will not open at all.</td>
</tr>
<tr>
<td><code>xwfp-</code></td>
<td>Workflow token</td>
<td>Short-lived, issued to a workflow step. Expires 15 minutes after issue or the moment the step completes - do not cache it.</td>
</tr>
</tbody></table>
<p>The two mistakes worth watching for: a Socket Mode connection that refuses to open despite a correct bot token is almost always missing the separate <code>xapp-</code> app-level token with <code>connections:write</code>, and a script that copies whichever token is closest at hand from the dashboard tends to grab the user token by accident, since it is listed right next to the bot token under <strong>OAuth &amp; Permissions</strong>. Both look, superficially, like a <a href="https://www.conferbot.com/glossary/term/oauth">OAuth</a> scope problem, and neither is fixed by adding scopes.</p>
<h3>Confirm which token you are actually holding</h3>
<p>Rather than guessing from the prefix, call <code>auth.test</code> with the token in question:</p>
<pre><code>curl -X POST https://slack.com/api/auth.test \
  -H "Authorization: Bearer YOUR_TOKEN"
</code></pre>
<p>The response identifies exactly what the token is - the workspace (<code>team</code>), the bot or user it belongs to (<code>user_id</code>, <code>bot_id</code> if applicable), and whether it is even valid. This is the fastest way to confirm a "permission denied" report is really a wrong-token report before you spend time auditing scopes that were never the problem. It is a single cheap request worth running before anything else in this section, and it takes less time than reading this paragraph.</p>
<h2>8. Workspace installs, org-wide apps and admin approval</h2>
<p>A bot that works in your development workspace and cannot be installed - or installs and sees nothing - in a customer's workspace is usually hitting an administrative control rather than a technical one.</p>
<h3>App approval is on by default in many organisations</h3>
<p>Workspace admins can require approval before any app is installed. Your OAuth flow completes, the user sees "request sent to your admin", and nothing works until someone approves it. There is no technical workaround - the install genuinely has not happened. If you distribute a Slack app, your onboarding needs to account for this delay explicitly rather than assuming installation is instant.</p>
<h3>Org-wide installs behave differently from workspace installs</h3>
<p>In Slack Enterprise Grid, an app can be installed to a single workspace or org-wide across many. Org-wide installs return a different token context, and code that assumes one <code>team_id</code> per token breaks - events arrive with varying team identifiers and API calls need the right context. If your app is only ever used in one workspace this never surfaces; the first Enterprise Grid customer surfaces it immediately.</p>
<h3>Token storage for multi-workspace apps</h3>
<p>A distributed app receives a distinct bot token per installation. Hardcoding a single token from your own workspace - which works throughout development - fails the moment a second workspace installs. Store tokens keyed by <code>team_id</code> (and <code>enterprise_id</code> for Grid) and look up the right one per incoming event.</p>
<h3>Token rotation</h3>
<p>Apps with token rotation enabled receive tokens that expire and must be refreshed with a refresh token. If your app worked for hours and then began returning <code>token_expired</code>, rotation is enabled and you are not refreshing. Either implement the refresh flow or disable rotation if your security posture allows.</p>
<h3>A note on Slack Connect channels</h3>
<p>Shared channels with external organisations impose extra restrictions. Bots may not see all messages in a Slack Connect channel, and some API methods behave differently. If your bot works in internal channels and misses messages in a shared one, this is why - and it is a platform constraint rather than a configuration you can change.</p>
<h2>Where Slack shows up as a real support channel</h2>
<p>Internal Slack bots absorb this guide's failure modes as a one-time cost - fix the scopes and reinstall once, and the bot mostly stays fixed. The calculation is different when Slack is a customer-facing or department-facing support surface rather than a hobby integration, because every new workflow risks reopening the scope-and-reinstall cycle. Slack shows up this way most often in <a href="https://www.conferbot.com/chatbot/slack/tech">tech companies</a> routing product and engineering questions, <a href="https://www.conferbot.com/chatbot/slack/hr">HR teams</a> fielding policy and benefits questions where our guide to <a href="https://www.conferbot.com/blog/slack-teams-chatbot-employee-support">Slack and Teams employee support</a> covers the conversation design side, <a href="https://www.conferbot.com/chatbot/slack/government">government agencies</a> running internal case coordination, and <a href="https://www.conferbot.com/chatbot/slack/manufacturing">manufacturing</a> teams using it for shift and maintenance coordination. If the workflow is IT ticketing specifically, <a href="https://www.conferbot.com/blog/slack-chatbot-it-support">Slack IT support</a> is the more direct read, and <a href="https://www.conferbot.com/blog/slack-chatbot-for-business">Slack chatbots for business</a> covers the broader case for the channel.</p>
<p>Slack is not the only workplace chat platform with this exact list of failure modes - <a href="https://www.conferbot.com/chatbot/teams">Microsoft Teams</a> has its own version, covered in <a href="https://www.conferbot.com/blog/microsoft-teams-chatbot-hr">Teams chatbots for HR</a>, and a business running both ends up maintaining two independent scope-and-webhook lifecycles for the same underlying conversation.</p>
<h2>When the plumbing stops being worth it</h2>
<p>Everything above is transport work: challenge handshakes, <a href="https://www.conferbot.com/glossary/term/oauth">OAuth</a> scope lifecycles, event subscription matrices, acknowledgement deadlines, retry deduplication, <a href="https://www.conferbot.com/glossary/term/rate-limiting">rate pacing</a>. It is the same list for every Slack app ever built, and none of it is your product.</p>
<p>That is a reasonable investment for an internal tool. It is a poor one when Slack is one of several channels you are expected to keep alive, because each platform has a completely different version of the same list. Which channels are worth keeping alive at all is the subject of our <a href="https://www.conferbot.com/blog/chatbot-channel-strategy">channel strategy guide</a>, and our guides to <a href="https://www.conferbot.com/blog/telegram-bot-not-responding">Telegram bots not responding</a>, <a href="https://www.conferbot.com/blog/discord-bot-not-responding">Discord bots not responding</a> and <a href="https://www.conferbot.com/blog/chat-widget-not-showing-on-website">chat widgets not showing on a website</a> exist precisely because those lists share nothing with this one. Our guide to <a href="https://www.conferbot.com/blog/chatbot-api-rate-limiting">chatbot API rate limiting</a> goes deeper on the pacing problem than any single platform's docs will.</p>
<p><a href="https://www.conferbot.com/chatbot/slack">Conferbot's Slack integration</a> is on the Business plan and handles installation, scopes, event delivery and acknowledgement timing for you. You build the conversation once in a visual flow editor and the same flow runs on <a href="https://www.conferbot.com/chatbot/teams">Microsoft Teams</a>, <a href="https://www.conferbot.com/chatbot/telegram">Telegram</a>, <a href="https://www.conferbot.com/chatbot/whatsapp">WhatsApp</a>, <a href="https://www.conferbot.com/chatbot/discord">Discord</a> and a <a href="https://www.conferbot.com/chatbot/website">website widget</a>, with formatting adapted per platform. Conversations can push data out through Conferbot's built-in <a href="https://www.conferbot.com/features/integrations-hub">integrations</a> - Webhook, Zapier, Slack, Mailchimp, Stripe, SalesForce, HubSpot, Google Sheets, Airtable, Calendly and Google Calendar - or through the <a href="https://www.conferbot.com/features/chatbot-api-integration">API</a> directly. If you would rather expose your own systems to the bot as callable tools, the <a href="https://www.conferbot.com/blog/chatbot-mcp-server-guide">chatbot MCP server guide</a> covers that pattern.</p>
<p><a href="https://app.conferbot.com"><strong>Start free with Conferbot</strong></a> - 600 conversations a month, no credit card. If you are debugging today, run the checklist above first; if you are about to write this plumbing for a third platform, that is the signal to stop.</p>
<p>Running the same bot inside Microsoft Teams? The failure list there is different again - manifest IDs, expiring client secrets, tenant policies and the @mention rule - and it is covered in our <a href="https://www.conferbot.com/blog/microsoft-teams-bot-not-responding">Microsoft Teams bot not responding guide</a>.</p>
<h2>FAQ</h2>
<p><strong>Why does Slack say my URL didn't respond with the value of the challenge parameter?</strong></p>
<p>Slack POSTs a url_verification payload containing a challenge string, and your endpoint must echo that value back within about 3 seconds. The usual cause is signature-verification or authentication middleware running first and rejecting the request - handle url_verification as the very first statement in your handler. Slack also does not follow redirects during verification, so register the final URL rather than one that 301s.</p>
<p><strong>How do I fix a Slack missing_scope error?</strong></p>
<p>The error response names exactly what you need in its needed field. Add that scope under OAuth &amp; Permissions, then reinstall the app to the workspace and use the newly issued token. Adding a scope without reinstalling changes nothing, because the token you already hold was issued with the old scope set - this is the most common Slack integration mistake.</p>
<p><strong>Why is my Slack bot not receiving any events?</strong></p>
<p>Check four things in order: that you actually subscribed to bot events rather than only verifying the URL; that you subscribed to the right message event, since public channels, private channels and DMs are separate subscriptions; that the bot has been invited to the channel with /invite @yourbot; and that Socket Mode is off, because it overrides your request URL entirely.</p>
<p><strong>Why does my Slack bot work in channels but not in DMs?</strong></p>
<p>Message events are split by conversation type and each is a distinct subscription. You need message.im for direct messages alongside message.channels for public channels, plus the im:history and im:write scopes. Subscribing to message.channels alone gives you exactly this symptom.</p>
<p><strong>Why does my Slack bot send the same message multiple times?</strong></p>
<p>Your handler is not returning HTTP 200 within Slack's 3-second window, so Slack retries up to three times and your code processes the event each time. Acknowledge immediately with res.sendStatus(200) and do the work afterwards. Also check the X-Slack-Retry-Num header and skip reprocessing when it is present, and deduplicate on event_id.</p>
<p><strong>What does Slack error not_in_channel mean?</strong></p>
<p>The bot is trying to post to a channel it has not joined. Invite it with /invite @yourbot in that channel. The chat:write.public scope allows posting to public channels without an invite, but it does not let the bot read them - you still need it in the channel to receive messages.</p>
<p><strong>What is the difference between Socket Mode and the Events API in Slack?</strong></p>
<p>Socket Mode delivers events over a WebSocket your app opens outbound, requiring no public endpoint, which makes it ideal for local development and firewalled environments. The Events API POSTs to a public HTTPS request URL. They are mutually exclusive - with Socket Mode enabled, Slack ignores your request URL completely, which catches out teams who enable it for development and forget to disable it before deploying.</p>
<p><strong>Which scopes does a Slack chatbot need?</strong></p>
<p>At minimum chat:write to send messages and app_mentions:read to receive mentions. Add im:history and im:write for direct messages, channels:history for public channel messages, groups:history for private channels (a separate scope), users:read to resolve names, and chat:write.public to post to public channels without an invite.</p>
<p><strong>Why is my Slack bot replying to itself in a loop?</strong></p>
<p>Bot messages generate message events too. Add <code>if (event.bot_id || event.subtype === 'bot_message') return;</code> as the first line of your handler. Without it the bot replies to its own reply indefinitely until Slack rate limits it, and two unguarded bots in one channel will generate thousands of messages in seconds.</p>
<p><strong>What does channel_not_found mean in Slack?</strong></p>
<p>Usually a channel name used where an ID was required - Slack channel IDs look like C01ABCDEF, and #general does not work in every endpoint. Store IDs rather than names, since names change and IDs do not. The same error also appears for private channels the bot has not been invited to, because it genuinely cannot see them.</p>
<p><strong>How long does a Slack app have to respond to an event?</strong></p>
<p>Three seconds to return HTTP 200. For slash commands and interactive components you can respond immediately with a placeholder and then update it using the response_url, which remains valid for 30 minutes and accepts up to five follow-up messages. Any handler that calls an AI model will exceed 3 seconds, so acknowledge first as a matter of course.</p>
<p><strong>What are Slack API rate limits for posting messages?</strong></p>
<p>Slack rate limits per method in tiers, and chat.postMessage is generally around one message per second per channel with short bursts tolerated. Exceeding it returns HTTP 429 with a Retry-After header in seconds, which you should honour exactly rather than retrying immediately.</p>
<hr />
<p>The full guide, with diagrams and the latest checks, is at <a href="https://www.conferbot.com/blog/slack-bot-not-responding">https://www.conferbot.com/blog/slack-bot-not-responding</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Discord Bot Not Responding? The Intents Problem and 7 Other Causes]]></title><description><![CDATA[Bot online but ignoring commands? Enable the Message Content intent. Then fix gateway 4014, token 4004, 50013 permissions and missing slash commands.]]></description><link>https://conferbot.hashnode.dev/discord-bot-not-responding-the-intents-problem-and-7-other-causes</link><guid isPermaLink="true">https://conferbot.hashnode.dev/discord-bot-not-responding-the-intents-problem-and-7-other-causes</guid><category><![CDATA[discord]]></category><category><![CDATA[bots]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[Tutorial]]></category><dc:creator><![CDATA[Conferbot]]></dc:creator><pubDate>Sun, 25 Oct 2026 05:07:02 GMT</pubDate><content:encoded><![CDATA[<p><em>Originally published on <a href="https://www.conferbot.com/blog/discord-bot-not-responding">conferbot.com</a>.</em></p>
<h2>First, separate the two symptoms - they have nothing in common</h2>
<p><strong>"Bot offline" and "bot online but silent" are completely different failures. Fixing one will never fix the other, and most wasted debugging time comes from treating them as the same problem.</strong></p>
<p><em>Last verified: August 2026 against the Discord Developer Portal and Gateway API. Error codes quoted are the literal values Discord returns.</em></p>
<table>
<thead>
<tr>
<th>Symptom</th>
<th>What it proves</th>
<th>Look at</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Grey / offline in member list</strong></td>
<td>No gateway connection exists</td>
<td>Token, process, close codes 4004 and 4014</td>
</tr>
<tr>
<td><strong>Green / online, ignores everything</strong></td>
<td>Connected, but not receiving or not permitted</td>
<td>Message Content intent, channel permissions</td>
</tr>
<tr>
<td><strong>Online, works in one channel only</strong></td>
<td>Connected and receiving; permission overwrite blocking</td>
<td>Channel-level permission overwrites</td>
</tr>
<tr>
<td><strong>Slash commands missing from menu</strong></td>
<td>Commands not registered, or still propagating</td>
<td>Global vs guild registration, <code>applications.commands</code> scope</td>
</tr>
</tbody></table>
<p>If your bot is green and ignoring messages, skip straight to the next section. That is the single most common Discord bot problem, and it has one cause. If instead you have a literal error code in hand, the <a href="https://www.conferbot.com/errors/discord">Discord error-code directory</a> maps every JSON error and gateway close code to its fix.</p>
<p><strong>Check the invite as well:</strong> if the bot is online but cannot post or moderate, paste its invite link into the <a href="https://www.conferbot.com/tools/discord-permissions-calculator">Discord permissions calculator</a> to see exactly which permissions it was added with. The Message Content intent is a separate switch in the Developer Portal, not a permission.</p>
<h2>1. The Message Content intent - the cause of most silent bots</h2>
<p>Since Discord's 2022 privacy changes, <strong>message content is a privileged intent</strong>. Without it, your bot still receives every <code>MESSAGE_CREATE</code> event, but the <code>content</code> field arrives as an empty string. Your command matching compares <code>""</code> against <code>"!help"</code>, finds no match, and does nothing - with no error, no exception and no log line.</p>
<p>That silence is what makes this so hard to diagnose. The bot is connected, events are flowing, your handler is running. It just cannot see what anyone typed.</p>
<h3>The fix requires two changes, and people usually only make one</h3>
<p><strong>Step 1 - enable it in the portal.</strong> Go to the <a href="https://discord.com/developers/applications">Discord Developer Portal</a>, open your application, select <strong>Bot</strong> in the sidebar, scroll to <strong>Privileged Gateway Intents</strong>, and toggle on <strong>Message Content Intent</strong>. Save.</p>
<p><strong>Step 2 - request it in your code.</strong> The portal toggle grants permission; your gateway connection must still ask for it. In discord.js:</p>
<pre><code>const { Client, GatewayIntentBits } = require('discord.js');

const client = new Client({
  intents: [
    GatewayIntentBits.Guilds,
    GatewayIntentBits.GuildMessages,
    GatewayIntentBits.MessageContent,   // the one people forget
  ],
});
</code></pre>
<p>In discord.py:</p>
<pre><code>import discord

intents = discord.Intents.default()
intents.message_content = True          # the one people forget

client = discord.Client(intents=intents)
</code></pre>
<p><strong>Step 3 - restart the process.</strong> Intents are negotiated when the gateway connection is established. Changing the portal toggle while the bot is running has no effect until it reconnects. A surprising number of "I enabled it and it still doesn't work" reports are just a bot that was never restarted.</p>
<h3>Three privileged intents, and what each unlocks</h3>
<table>
<thead>
<tr>
<th>Intent</th>
<th>Needed for</th>
<th>Symptom without it</th>
</tr>
</thead>
<tbody><tr>
<td>Message Content</td>
<td>Reading what users type</td>
<td>Prefix commands silently never match</td>
</tr>
<tr>
<td>Server Members</td>
<td>Join/leave events, full member lists</td>
<td>Welcome messages never fire; role automation breaks</td>
</tr>
<tr>
<td>Presence</td>
<td>Online status and activity tracking</td>
<td>Status-based features see everyone as offline</td>
</tr>
</tbody></table>
<h3>The 100-server wall</h3>
<p>Once a bot reaches <strong>100 servers it must be verified</strong> to continue using privileged intents. If you are approaching that number, apply early - verification is a manual review and takes time. Unverified bots are capped at 100 servers (the API refuses further joins with error <a href="https://www.conferbot.com/errors/discord/30001">30001</a>), and privileged intents need approval once a bot reaches the review threshold - Discord's docs currently phrase it as more than 10,000 unique users, older guidance said 100 servers. Crossing that line without approval means the intents switch off and the bot goes silent for its entire user base at once, which is a memorable way to discover this rule.</p>
<h3>Or sidestep it: use slash commands</h3>
<p>Slash command interactions deliver their arguments directly in the interaction payload, so <strong>they work without the Message Content intent at all</strong>. If your bot only needs explicit commands rather than reading ambient conversation, migrating from prefix commands to slash commands removes this entire problem - and removes the 100-server verification requirement along with it.</p>
<h2>2. Gateway error 4014: disallowed intents</h2>
<p>If the bot never comes online and the connection drops immediately, check for this close code:</p>
<pre><code>Gateway closed: 4014 - Disallowed intent(s)
</code></pre>
<p>This is the exact inverse of the previous section: your code is <em>requesting</em> a privileged intent that the portal has <em>not</em> granted. Discord refuses the connection outright rather than downgrading it. The <a href="https://www.conferbot.com/errors/discord/gateway-4014">gateway 4014 reference page</a> has the library-specific fixes.</p>
<p>Two fixes, depending on what you actually need:</p>
<ul>
<li>Enable the intent in the Developer Portal (Bot -&gt; Privileged Gateway Intents), or</li>
<li>Remove it from your intents list in code if the bot does not genuinely require it</li>
</ul>
<p>The frequent trigger is copying a starter template that requests <code>Intents.all()</code> or every <code>GatewayIntentBits</code> value. That works on a fresh unverified bot only if you have toggled all three privileges on. Request the minimum set your features need - it connects more reliably and delays the verification requirement.</p>
<h3>The neighbouring close codes</h3>
<table>
<thead>
<tr>
<th>Code</th>
<th>Meaning</th>
<th>Fix</th>
</tr>
</thead>
<tbody><tr>
<td>4004</td>
<td>Authentication failed</td>
<td>Token is wrong, revoked, or has whitespace. Regenerate and redeploy.</td>
</tr>
<tr>
<td>4013</td>
<td>Invalid intent(s)</td>
<td>The intent bitfield contains an undefined value. Usually a hand-computed number.</td>
</tr>
<tr>
<td>4014</td>
<td>Disallowed intent(s)</td>
<td>Requesting a privileged intent not enabled in the portal.</td>
</tr>
<tr>
<td>4008</td>
<td>Rate limited</td>
<td>Too many gateway payloads. Usually a reconnect loop.</td>
</tr>
</tbody></table>
<p>Note the difference between 4013 and 4014: 4013 means the value is not a real intent, 4014 means it is real but you are not allowed it. If you are constructing the bitfield by hand, stop - use the library's named constants.</p>
<h2>3. Token invalid, and the Git commit that killed it</h2>
<p>Close code <a href="https://www.conferbot.com/errors/discord/gateway-4004">4004</a> or an immediate <code>Improper token has been passed</code> means the token is bad. Beyond the usual copy-paste errors, one cause is worth calling out because it produces a bot that worked yesterday and does not today:</p>
<p><strong>Discord automatically invalidates tokens it finds in public repositories.</strong> Their scanner monitors GitHub and other public sources. Commit your token, and it stops working - often within minutes, with no notification beyond an email to the application owner. If your bot died suddenly and nothing changed on your side, check whether the token reached a public repo, a Pastebin, a screenshot in a support thread, or a Discord message in a public channel.</p>
<p>The remedy is the same either way: regenerate the token in the Developer Portal, move it into an environment variable or secrets manager, and add the file that held it to <code>.gitignore</code>. Regenerating immediately invalidates the previous token, so redeploy everywhere at once or you will have half your instances failing.</p>
<p>Other reliable token failures:</p>
<ul>
<li><strong>Client secret used instead of bot token.</strong> They are different strings on adjacent pages of the portal. The client secret is for OAuth flows and will never authenticate a gateway connection.</li>
<li><strong>The <code>Bot </code> prefix.</strong> The Authorization header needs <code>Bot &lt;token&gt;</code> with a space. Most libraries add this for you - if you are calling the REST API directly with fetch or curl, you must add it yourself.</li>
<li><strong>Trailing newline from a secrets file.</strong> Trim the value before use.</li>
</ul>
<h2>4. Missing Permissions (50013) and Missing Access (50001)</h2>
<p>Your bot is online, receiving events, and its replies never appear. The API is almost certainly returning:</p>
<pre><code>{"message": "Missing Permissions", "code": 50013}
{"message": "Missing Access", "code": 50001}
</code></pre>
<p>Discord permissions resolve through several layers, and a bot can hold a permission at server level while being denied it in the exact channel where you are testing:</p>
<ol>
<li><code>@everyone</code> server-wide permissions</li>
<li>Permissions from each role the bot holds</li>
<li><strong>Channel-level overwrites</strong> - these override the above and are where the problem usually lives</li>
<li>Category overwrites, inherited by channels that sync with the category</li>
</ol>
<p>A single deny on a channel overwrite beats any allow granted higher up. Denies win. The <a href="https://www.conferbot.com/errors/discord/50013">50013 reference page</a> walks the overwrite layers in order. <code>50001</code> is the channel-visibility version of the same problem and has its own page at <a href="https://www.conferbot.com/errors/discord/50001">Missing Access</a>.</p>
<h3>Check it the fast way</h3>
<p>Server Settings -&gt; Roles -&gt; select your bot's role -&gt; the permission list shows server-wide grants. Then right-click the specific channel -&gt; Edit Channel -&gt; Permissions, and look for the bot's role or the bot itself with a red X.</p>
<p>The permissions a conversational bot needs in each channel it works in:</p>
<ul>
<li><strong>View Channel</strong> - without it the bot cannot see the channel exists</li>
<li><strong>Send Messages</strong> - the obvious one</li>
<li><strong>Read Message History</strong> - required to reply to a specific message</li>
<li><strong>Embed Links</strong> - rich embeds are silently dropped without it</li>
<li><strong>Attach Files</strong> - only if you send images or documents</li>
<li><strong>Use External Emojis</strong> - if your replies use emojis from other servers</li>
</ul>
<h3>The role hierarchy rule</h3>
<p>A bot can only manage roles <em>below</em> its own highest role in the list. A moderation bot that cannot assign a role, kick a member or add a nickname is usually positioned too low in Server Settings -&gt; Roles. Drag the bot's role above the roles it needs to manage. This produces the specific symptom of a bot that moderates most members successfully and fails on the ones with elevated roles.</p>
<h3>Threads and forums are separate</h3>
<p>Sending in a thread requires <strong>Send Messages in Threads</strong>, which is a distinct permission from Send Messages. A bot that works in a normal channel and fails in a thread has hit exactly this. Archived threads must also be unarchived before a message can post to them.</p>
<h2>5. Slash commands that never appear</h2>
<p>Registered commands that do not show in the client have three usual causes, in this order.</p>
<h3>The applications.commands scope was missing from the invite</h3>
<p>This is the big one, and it is invisible after the fact. If you invited the bot with a URL that requested only the <code>bot</code> scope, the server never authorised it to register slash commands - and no amount of re-registering fixes it. Your invite URL needs both:</p>
<pre><code>https://discord.com/api/oauth2/authorize?client_id=YOUR_ID&amp;permissions=YOUR_PERMS&amp;scope=bot%20applications.commands
</code></pre>
<p>The fix is to re-invite the bot using a URL that includes <code>applications.commands</code>. You do not need to kick it first - visiting the corrected URL and re-authorising adds the missing scope to the existing installation.</p>
<h3>Global commands take up to an hour</h3>
<p>Global commands propagate across Discord's infrastructure and can take up to <strong>one hour</strong> to appear. Guild-scoped commands register <strong>instantly</strong>. During development, always register to a single test guild - otherwise you will spend an afternoon convinced your registration code is broken when it worked the first time.</p>
<pre><code>// Instant - use during development
await rest.put(Routes.applicationGuildCommands(clientId, testGuildId), { body: commands });

// Up to 1 hour - use for production release
await rest.put(Routes.applicationCommands(clientId), { body: commands });
</code></pre>
<h3>Command permissions are restricting visibility</h3>
<p>Server admins can restrict which roles and channels a command appears in, via Server Settings -&gt; Integrations -&gt; your bot. A command that is invisible to regular members but visible to you as an admin has been restricted here, not broken in code.</p>
<h3>The 3-second interaction deadline</h3>
<p>A separate and very common failure: <strong>you must acknowledge an interaction within 3 seconds</strong> or Discord shows "The application did not respond" and the interaction token becomes invalid. Any bot that calls an AI model, queries a database or hits an external API will exceed this.</p>
<p>Defer immediately, then follow up:</p>
<pre><code>await interaction.deferReply();          // buys you up to 15 minutes
const answer = await callSlowThing();    // now take your time
await interaction.editReply(answer);
</code></pre>
<p>This single pattern accounts for most "my AI bot says it did not respond but the logs show it worked" reports.</p>
<h2>6. DMs, mentions and the events you are not subscribed to</h2>
<p>Guild messages and direct messages arrive through <strong>different intents</strong>. A bot subscribed to <code>GuildMessages</code> but not <code>DirectMessages</code> works perfectly in servers and ignores every DM:</p>
<pre><code>intents: [
  GatewayIntentBits.Guilds,
  GatewayIntentBits.GuildMessages,
  GatewayIntentBits.DirectMessages,     // separate subscription
  GatewayIntentBits.MessageContent,
]
</code></pre>
<p>Two more DM-specific rules catch people out:</p>
<ul>
<li><strong>Users can block DMs from server members</strong> in their privacy settings. Sending to someone with this enabled returns <code>Cannot send messages to this user</code> (code <a href="https://www.conferbot.com/errors/discord/50007">50007</a>). There is no way to detect this in advance and no workaround - it is a user preference.</li>
<li><strong>In discord.js, DM channels may need partials enabled</strong> to receive events for channels that were not cached at startup. Without <code>Partials.Channel</code>, DMs from users the bot has not interacted with in that session can be missed.</li>
</ul>
<h3>Mention matching that fails on nicknames</h3>
<p>If your bot responds to being @-mentioned, do not string-match the mention text. When a user has a server nickname, the mention renders differently, and matching on <code>&lt;@123456&gt;</code> will miss <code>&lt;@!123456&gt;</code>. Use the library's mention helper - <code>message.mentions.has(client.user)</code> in discord.js - which handles both forms.</p>
<h3>The infinite loop guard</h3>
<p>Always ignore messages from bots, including your own, as the first line of your handler:</p>
<pre><code>if (message.author.bot) return;
</code></pre>
<p>Without it, a bot that replies to messages will reply to its own reply, forever, until Discord rate limits it. Two bots in the same channel without this guard will do it to each other and generate thousands of messages in seconds.</p>
<h2>7. Rate limits, reconnect loops and sharding</h2>
<p>Discord returns <a href="https://www.conferbot.com/errors/discord/http-429">429</a> with a <code>retry_after</code> value in seconds. Well-maintained libraries queue and retry automatically, so if you are seeing raw 429s you are probably calling the REST API directly.</p>
<p>Two limits matter more than the general one (the full set of REST, gateway and interaction ceilings is on the <a href="https://www.conferbot.com/limits/discord">Discord limits page</a>):</p>
<ul>
<li><strong>Global:</strong> around 50 requests per second across your whole application.</li>
<li><strong>Per route:</strong> tighter and specific. Message sends in a single channel are limited to roughly 5 per 5 seconds, which is easy to hit with a bot that splits replies into several bubbles.</li>
</ul>
<p>Exceeding limits repeatedly can trigger a <strong>Cloudflare ban at the IP level</strong>, which returns HTML rather than JSON and lasts an hour or more. If your bot suddenly gets unparseable responses from the API, this is why - and it usually follows a reconnect loop.</p>
<h3>Identify limits and the crash loop</h3>
<p>Gateway identifies are limited to roughly <strong>1,000 per day</strong>. A bot crash-looping on startup - throwing an exception after connecting, being restarted by the supervisor, connecting again - burns through that budget and gets locked out of connecting entirely. If a bot has been failing all day and now cannot connect at all even after the bug is fixed, wait for the daily reset.</p>
<h3>Sharding at 2,500 guilds</h3>
<p>Bots in <strong>2,500 or more guilds must shard</strong>. Discord refuses the connection otherwise. Most libraries provide a sharding manager that handles this, but it is a deliberate step you have to take - it does not happen automatically as you grow.</p>
<h2>8. HTTP interactions: the other way a bot can go silent</h2>
<p>Everything above assumes your bot holds a persistent <a href="https://www.conferbot.com/glossary/term/websocket">WebSocket</a> connection to the gateway, which is how discord.js and discord.py work by default. Discord also supports a second, gateway-free transport for slash commands: an <strong>Interactions Endpoint URL</strong>, configured under <strong>General Information</strong> in the Developer Portal. Instead of pushing interaction payloads down a socket, Discord POSTs each one to your HTTPS endpoint - the same shape as a <a href="https://www.conferbot.com/glossary/term/webhook">webhook</a>. Bots built on serverless functions often use this so they never need a long-running process.</p>
<p>This path fails differently, and more silently, than a gateway disconnect.</p>
<h3>The PING/PONG handshake</h3>
<p>The moment you save an Interactions Endpoint URL, Discord sends a synthetic <code>PING</code> payload (<code>type: 1</code>) to it and expects a <code>200</code> response containing a <code>PONG</code> (also <code>type: 1</code>) within a few seconds. Fail that check and Discord refuses to save the URL at all - there is no partial state where the field is set but broken, so if your endpoint field is empty, this handshake is what rejected it.</p>
<h3>Signature verification, not a bearer token</h3>
<p>Every request Discord sends to an interactions endpoint carries <code>X-Signature-Ed25519</code> and <code>X-Signature-Timestamp</code> headers. You must verify them - by concatenating the timestamp with the raw request body and checking the Ed25519 signature against your application's public key - <em>before</em> parsing the JSON. Skipping this, or verifying against the parsed body instead of the raw bytes, produces a rejected handshake that looks identical to a network problem. Discord deliberately sends malformed test requests to confirm you are actually checking the signature rather than always returning 200.</p>
<table>
<thead>
<tr>
<th>Transport</th>
<th>How updates arrive</th>
<th>Typical failure</th>
</tr>
</thead>
<tbody><tr>
<td>Gateway (default)</td>
<td>Persistent WebSocket, all event types</td>
<td>Close codes 4004/4013/4014, missing intents</td>
</tr>
<tr>
<td>HTTP interactions endpoint</td>
<td>Discord POSTs to your URL, interactions only</td>
<td>Failed PING/PONG, bad signature verification, endpoint not public</td>
</tr>
</tbody></table>
<p>Because this endpoint only ever carries interactions - never raw <code>MESSAGE_CREATE</code> events - a bot built entirely on it can never read prefix commands, regardless of intents. If you switched to this transport and prefix commands stopped working while slash commands kept working, that is expected, not a regression. It is also the same 3-second acknowledgement deadline as the gateway path: return a <code>DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE</code> response type immediately if the real answer needs an <a href="https://www.conferbot.com/features/chatbot-api-integration">API call</a> or model inference.</p>
<p>If the endpoint fails Discord's validation and you are not sure whether the fault is the Ed25519 check, the PING handler or the network in front of it, our <a href="https://www.conferbot.com/blog/webhook-not-firing-debugging-guide">webhook debugging guide</a> has a platform-neutral order for isolating which of the three it is.</p>
<h2>Running Discord as a support or sales channel, not just a community server</h2>
<p>Most of this guide describes problems that show up whether the bot is a hobby project or a revenue channel. But the two use it very differently. A community bot can absorb an afternoon of gateway debugging as a side quest; a business relying on Discord for support, onboarding or sales cannot, because every hour spent on intents and close codes is an hour not spent on the conversation itself.</p>
<p>Discord shows up as a real support and community surface across a specific set of industries more than others: <a href="https://www.conferbot.com/chatbot/discord/education">education</a>, where servers double as course cohorts and study groups; <a href="https://www.conferbot.com/chatbot/discord/media">media and creator businesses</a>, where a server is the primary fan community; <a href="https://www.conferbot.com/chatbot/discord/tech">tech companies</a>, where Discord has become the default place developer communities live; and <a href="https://www.conferbot.com/chatbot/discord/nonprofit">nonprofits</a>, who use it as a low-cost volunteer and donor hub. If community moderation and growth - rather than transport debugging - is the actual problem you are solving, our guide to <a href="https://www.conferbot.com/blog/discord-bot-community-management">Discord bot community management</a> is the more relevant next read.</p>
<h3>What changes when you stop hand-rolling the transport</h3>
<p><a href="https://www.conferbot.com/chatbot/discord">Conferbot's Discord integration</a> is on the Pro plan and up, and it removes every failure mode in this guide from your side of the line: gateway connection, intent negotiation, permission checks and interaction deadlines are handled by the platform, so a flow change is a visual edit rather than a redeploy. Conversations can also branch out to real systems through Conferbot's fixed integration set - <a href="https://www.conferbot.com/features/integrations-hub">Webhook, Zapier, Slack, Mailchimp, Stripe, SalesForce, HubSpot, Google Sheets, Airtable, Calendly and Google Calendar</a> - or through the raw <a href="https://www.conferbot.com/features/chatbot-api-integration">API</a> if you need something the built-in integrations do not cover. There is no bring-your-own retrieval pipeline and no canned-response library bolted on; a flow is a flow, and it runs the same way on Discord as it does everywhere else it deploys.</p>
<p>The invite URL that grants a bot the <code>applications.commands</code> scope is itself an <a href="https://www.conferbot.com/glossary/term/oauth">OAuth 2.0</a> authorization request - the same protocol Slack, Google and most SaaS integrations use for delegated access, which is worth knowing if you are debugging why a scope change did not take effect: like Slack, some scope changes require the authorization to be redone, not just re-saved.</p>
<h2>The diagnostic order that finds it fastest</h2>
<table>
<thead>
<tr>
<th>#</th>
<th>Question</th>
<th>If no</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>Is the bot green in the member list?</td>
<td>Token (4004) or intents (4014). Read the gateway close code.</td>
</tr>
<tr>
<td>2</td>
<td>Does <code>console.log</code> in your message handler print anything?</td>
<td>Wrong intent for the event source - guild vs DM.</td>
</tr>
<tr>
<td>3</td>
<td>Is <code>message.content</code> non-empty when it prints?</td>
<td><strong>Message Content intent.</strong> Portal toggle + code + restart.</td>
</tr>
<tr>
<td>4</td>
<td>Does a manual send from that handler succeed?</td>
<td>Permissions - 50013 or 50001. Check channel overwrites.</td>
</tr>
<tr>
<td>5</td>
<td>Do slash commands appear in the menu?</td>
<td>Missing <code>applications.commands</code> scope, or global propagation delay.</td>
</tr>
<tr>
<td>6</td>
<td>Do interactions respond within 3 seconds?</td>
<td>Call <code>deferReply()</code> first, then <code>editReply()</code>.</td>
</tr>
<tr>
<td>7</td>
<td>Are you logging full API error bodies?</td>
<td>Discord's error codes are precise. Do not discard them.</td>
</tr>
</tbody></table>
<p>Step 3 is the one to reach for first when the bot is online. Printing <code>message.content</code> and seeing an empty string is the definitive confirmation of the intents problem, and it takes ten seconds to check.</p>
<h2>When to stop maintaining the plumbing</h2>
<p>Everything above is transport-layer work: intent negotiation, gateway close codes, permission resolution, interaction deadlines, shard counts, <a href="https://www.conferbot.com/glossary/term/rate-limiting">rate limiting</a>, identify budgets. None of it is your product. It is the toll for operating the Discord <a href="https://www.conferbot.com/glossary/term/api-endpoint">API</a> directly, and it is the same toll for every bot on the platform - our guides to <a href="https://www.conferbot.com/blog/telegram-bot-not-responding">Telegram bots not responding</a> and <a href="https://www.conferbot.com/blog/slack-bot-not-responding">Slack bots not responding</a> exist because those platforms make you pay an entirely different version of the same tax, and our piece on <a href="https://www.conferbot.com/blog/chatbot-api-rate-limiting">API rate limiting</a> covers the pacing problem in more depth than any single-platform guide can.</p>
<p>That is a fine trade for a community bot you enjoy building. It is a worse one when Discord is a support or sales channel for a business, and considerably worse when Discord is one of several channels you are expected to keep alive - because each platform has an entirely different version of this same list. If your visitors also reach you through a website, the same class of debugging shows up there too; see our guide to a <a href="https://www.conferbot.com/blog/chat-widget-not-showing-on-website">chat widget not showing on your website</a> when that is the channel giving you trouble.</p>
<p><a href="https://www.conferbot.com/chatbot/discord">Conferbot's Discord integration</a> handles connection, intents, permissions and interaction deadlines for you: you connect the bot and build the conversation in a visual flow editor rather than in gateway code. The same flow runs on <a href="https://www.conferbot.com/chatbot/telegram">Telegram</a>, <a href="https://www.conferbot.com/chatbot/whatsapp">WhatsApp</a>, <a href="https://www.conferbot.com/chatbot/slack">Slack</a> and a <a href="https://www.conferbot.com/chatbot/website">website widget</a>, with buttons and media adapting to each platform automatically.</p>
<p><a href="https://app.conferbot.com"><strong>Start free with Conferbot</strong></a> - the free plan includes 600 conversations a month and needs no credit card, so you can have the same flow running on a second channel this afternoon.</p>
<p>If you are debugging today, work the table above - if the bot is online and silent, it is the Message Content intent far more often than anything else. If you are on your third channel and writing the same plumbing a third time, that is the signal to stop.</p>
<h2>FAQ</h2>
<p><strong>Why is my Discord bot online but not responding to commands?</strong></p>
<p>Almost always the Message Content intent. Since 2022 it is a privileged intent, and without it your bot still receives message events but the content field arrives empty, so command matching silently never matches. Enable Message Content Intent in the Developer Portal under Bot -&gt; Privileged Gateway Intents, add it to your intents array in code, and restart the process - all three steps are required.</p>
<p><strong>How do I fix Discord gateway error 4014?</strong></p>
<p>Error 4014 means Disallowed intent(s): your code requests a privileged intent that is not enabled in the Developer Portal. Either enable that intent under Bot -&gt; Privileged Gateway Intents, or remove it from your intents list if the bot does not need it. It is commonly caused by copying a template that requests Intents.all().</p>
<p><strong>What is the difference between Discord gateway errors 4013 and 4014?</strong></p>
<p>4013 means Invalid intent(s) - the bitfield contains a value that is not a real intent, usually from hand-computing the number. 4014 means Disallowed intent(s) - the intent is real but your application has not been granted it in the Developer Portal. Use your library's named intent constants to avoid 4013 entirely.</p>
<p><strong>Why does my Discord bot send no messages despite being online?</strong></p>
<p>The API is likely returning code 50013 Missing Permissions or 50001 Missing Access. Channel-level permission overwrites beat server-wide role grants, so a bot can hold Send Messages at server level and be denied it in the specific channel you are testing. Check the channel's permission settings for a red X against the bot's role.</p>
<p><strong>Why don't my slash commands appear in Discord?</strong></p>
<p>Three usual causes. The invite URL omitted the applications.commands scope, so the server never authorised command registration - re-invite with scope=bot%20applications.commands. Or you registered globally, which takes up to an hour to propagate; register to a test guild during development for instant results. Or a server admin restricted the command under Server Settings -&gt; Integrations.</p>
<p><strong>What does "The application did not respond" mean in Discord?</strong></p>
<p>Your bot did not acknowledge the interaction within Discord's 3-second deadline, so the interaction token expired. Call deferReply() immediately on receiving the interaction, which extends your window to 15 minutes, then send the real answer with editReply(). This is the standard fix for any bot that calls an AI model or external API.</p>
<p><strong>Do I need the Message Content intent for slash commands?</strong></p>
<p>No. Slash command arguments arrive in the interaction payload itself, so slash commands work without the Message Content intent entirely. Migrating from prefix commands to slash commands removes this problem and also avoids the privileged-intent verification requirement at 100 servers.</p>
<p><strong>Why did my Discord bot token suddenly stop working?</strong></p>
<p>Discord scans public sources including GitHub and automatically invalidates any bot token it finds. If your bot died with no change on your side, check whether the token reached a public repo, a screenshot, or a public message. Regenerate the token, move it into an environment variable, and redeploy everywhere at once since regenerating invalidates the old one immediately.</p>
<p><strong>Why can't my Discord bot assign roles?</strong></p>
<p>Role hierarchy. A bot can only manage roles positioned below its own highest role in Server Settings -&gt; Roles. If it moderates most members successfully but fails on those with elevated roles, drag the bot's role above the roles it needs to manage. The bot also needs the Manage Roles permission itself.</p>
<p><strong>Why does my Discord bot work in servers but ignore DMs?</strong></p>
<p>Guild messages and direct messages use separate intents. Add GatewayIntentBits.DirectMessages alongside GuildMessages. In discord.js you may also need Partials.Channel to receive events for DM channels that were not cached at startup. Note that users can also block DMs from server members, which returns error 50007 with no workaround.</p>
<p><strong>When does a Discord bot need to be verified?</strong></p>
<p>At 100 servers. Beyond that, privileged intents require verification, which is a manual review - apply early, because a bot that reaches 100 servers unverified loses privileged intents and goes silent for its entire user base at once. Separately, bots in 2,500 or more guilds must implement sharding.</p>
<p><strong>Why is my Discord bot repeating itself in a loop?</strong></p>
<p>It is responding to its own messages. Add <code>if (message.author.bot) return;</code> as the first line of your message handler. Without that guard a bot replies to its own reply indefinitely until Discord rate limits it, and two unguarded bots in one channel will generate thousands of messages in seconds.</p>
<hr />
<p>The full guide, with diagrams and the latest checks, is at <a href="https://www.conferbot.com/blog/discord-bot-not-responding">https://www.conferbot.com/blog/discord-bot-not-responding</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Telegram Bot Not Responding? Every Cause, Ranked by How Often It Happens]]></title><description><![CDATA[Bot online but silent? Run getWebhookInfo first: a set webhook makes getUpdates fail with 409. Then check group privacy mode, 403 blocks, 429s and tok]]></description><link>https://conferbot.hashnode.dev/telegram-bot-not-responding-every-cause-ranked-by-how-often-it-happens</link><guid isPermaLink="true">https://conferbot.hashnode.dev/telegram-bot-not-responding-every-cause-ranked-by-how-often-it-happens</guid><category><![CDATA[telegram]]></category><category><![CDATA[bots]]></category><category><![CDATA[api]]></category><category><![CDATA[debugging]]></category><dc:creator><![CDATA[Conferbot]]></dc:creator><pubDate>Sun, 25 Oct 2026 04:41:17 GMT</pubDate><content:encoded><![CDATA[<p><em>Originally published on <a href="https://www.conferbot.com/blog/telegram-bot-not-responding">conferbot.com</a>.</em></p>
<h2>Start here: one command tells you which half of the problem you have</h2>
<p><strong>Before you change any code, call <code>getWebhookInfo</code>. Its response splits every "bot not responding" problem into two families, and the fix is completely different for each.</strong></p>
<p><em>Last verified: August 2026 against the Telegram Bot API. Error strings quoted below are the literal responses the API returns.</em></p>
<p>Open this in a browser, replacing <code>&lt;TOKEN&gt;</code> with your bot token:</p>
<pre><code>https://api.telegram.org/bot&lt;TOKEN&gt;/getWebhookInfo
</code></pre>
<p>Read the <code>url</code> field in the JSON that comes back:</p>
<table>
<thead>
<tr>
<th>What <code>url</code> says</th>
<th>What it means</th>
<th>Where to look next</th>
</tr>
</thead>
<tbody><tr>
<td><code>""</code> (empty)</td>
<td>No webhook set. Your bot must be polling with <code>getUpdates</code>.</td>
<td>Is your process actually running? Is something else polling the same token?</td>
</tr>
<tr>
<td>A URL you recognise</td>
<td>Webhook mode. Telegram is pushing to that address.</td>
<td>Check <code>last_error_message</code> and <code>pending_update_count</code> in the same response.</td>
</tr>
<tr>
<td>A URL you do <em>not</em> recognise</td>
<td>An old deploy, a teammate's tunnel, or a stale staging environment is stealing your updates.</td>
<td>Delete it. This is the single most common cause of a silent bot.</td>
</tr>
</tbody></table>
<p>Two more fields in that response are worth more than an hour of debugging:</p>
<ul>
<li><code>pending_update_count</code> - if this number is large and climbing, Telegram has updates for you and nothing is collecting them. Your bot is not being ignored by Telegram; your bot is not listening.</li>
<li><code>last_error_message</code> - if this is populated, Telegram tried to deliver and your server rejected it. The message is usually blunt and correct: <code>Connection refused</code>, <code>SSL error</code>, <code>Wrong response from the webhook: 502 Bad Gateway</code>.</li>
</ul>
<p>The rest of this guide walks the nine causes in rough order of how often they actually turn out to be the answer. If you have a literal error string in hand, the <a href="https://www.conferbot.com/errors/telegram">Telegram error-code directory</a> maps each one to its fix.</p>
<p><strong>Fastest first check:</strong> the <a href="https://www.conferbot.com/tools/telegram-webhook-checker">Telegram webhook checker</a> reads getWebhookInfo for you, explains pending_update_count and the last delivery error in plain English, and can reset or delete the webhook from your browser. If the bot answers but in the wrong chat, the <a href="https://www.conferbot.com/tools/telegram-chat-id-finder">chat ID finder</a> lists every chat it can see.</p>
<h2>1. The 409 conflict: getUpdates and webhook cannot coexist</h2>
<p>The exact error is:</p>
<pre><code>{"ok":false,"error_code":409,"description":"Conflict: can't use getUpdates method while webhook is active; use deleteWebhook to delete the webhook first"}
</code></pre>
<p>Telegram permits exactly one consumer of updates per bot token. You either poll with <code>getUpdates</code> or you receive pushes at a webhook. Never both. When a webhook is registered and your code starts polling, the poll fails with 409 and your bot goes silent - even though the process is running and the logs may look healthy, because many libraries swallow this error and retry forever. The <a href="https://www.conferbot.com/errors/telegram/409-conflict-cant-use-getupdates-method-while-webhook-is-active">409 reference page</a> covers this variant on its own.</p>
<p>To switch to polling, remove the webhook first:</p>
<pre><code>https://api.telegram.org/bot&lt;TOKEN&gt;/deleteWebhook?drop_pending_updates=true
</code></pre>
<p>The <code>drop_pending_updates=true</code> parameter matters more than people expect. Without it, every message queued while the bot was down gets delivered the instant you start polling. A bot that was offline for two days will replay two days of conversations at once, sending welcome messages and notifications to people who moved on long ago. Include the flag unless you specifically want the backlog.</p>
<h3>The variant nobody expects: two copies of your own bot</h3>
<p>A 409 also appears with a different description when two processes poll the same token:</p>
<pre><code>Conflict: terminated by other getUpdates request; make sure that only one bot instance is running
</code></pre>
<p>This is the classic symptom of a bot that responds to roughly half the messages, seemingly at random. Two instances are both long-polling; each message goes to whichever one happened to be holding the connection. Common sources: a local dev process still running while production is live, a container that was scaled to 2 replicas, a <code>nodemon</code> or <code>watchdog</code> restart that left the old process alive, or a free-tier host that spun up a second dyno.</p>
<p>Polling does not scale horizontally. If you need more than one instance, you need webhooks and a load balancer in front of them. The two-process variant has its own page at <a href="https://www.conferbot.com/errors/telegram/409-conflict-terminated-by-other-getupdates-request">terminated by other getUpdates request</a>.</p>
<h2>2. Privacy mode: why the bot works in DMs but ignores your group</h2>
<p><strong>If your bot replies fine in a one-to-one chat but is completely silent in a group, privacy mode is the cause about nine times out of ten.</strong></p>
<p>By default, a bot added to a group receives only a narrow slice of messages:</p>
<ul>
<li>Messages that start with a slash command (<code>/start</code>, <code>/help</code>)</li>
<li>Messages that explicitly @-mention the bot</li>
<li>Replies to one of the bot's own messages</li>
<li>Service messages (people joining, leaving, being promoted)</li>
</ul>
<p>Everything else - ordinary conversation between members - is never delivered to your bot at all. This is a deliberate privacy protection, not a bug, and it means no amount of code fixing will help. Your handler is correct; the update never arrives.</p>
<p>To turn it off, talk to <a href="https://t.me/BotFather">@BotFather</a>:</p>
<pre><code>/setprivacy  -&gt;  select your bot  -&gt;  Disable
</code></pre>
<p>Then comes the step that trips up most people: <strong>the change does not apply to groups the bot is already in</strong>. You must remove the bot from the group and re-add it. Until you do, it keeps operating under the privacy setting that was active when it joined.</p>
<p>One alternative is worth knowing: promoting the bot to group admin also grants it visibility of all messages, regardless of the privacy setting. If you cannot re-add the bot for social reasons - a large community that would notice - promoting it to admin achieves the same result.</p>
<h3>The command-suffix trap in groups</h3>
<p>In a group containing more than one bot, Telegram clients append the bot username to commands: a user tapping <code>/help</code> actually sends <code>/help@YourBotName</code>. Handlers matching the literal string <code>/help</code> will not fire. Match on the command prefix and strip anything after <code>@</code>, or use a framework helper that normalises this for you. This produces the specific and confusing symptom of a bot that works in a test group and fails in the real one.</p>
<h2>3. The webhook is set, and quietly failing every delivery</h2>
<p>When <code>getWebhookInfo</code> shows a URL plus a populated <code>last_error_message</code>, Telegram is doing its job and your endpoint is refusing the handoff. The messages are terse but accurate. Here is what each one actually means.</p>
<table>
<thead>
<tr>
<th><code>last_error_message</code></th>
<th>Real cause</th>
<th>Fix</th>
</tr>
</thead>
<tbody><tr>
<td>SSL error / certificate verify failed</td>
<td>Expired cert, or a chain missing its intermediate certificate</td>
<td>Test with SSL Labs. Browsers forgive an incomplete chain; Telegram does not.</td>
</tr>
<tr>
<td>Connection refused</td>
<td>Nothing listening, or a firewall blocking Telegram's IP ranges</td>
<td>Confirm the port is open to the public internet, not just your VPC.</td>
</tr>
<tr>
<td>Wrong response from the webhook: 502 / 503</td>
<td>Your app is crashing or cold-starting on each delivery</td>
<td>Check application logs at the timestamp in <code>last_error_date</code>.</td>
</tr>
<tr>
<td>Read timeout expired</td>
<td>Your handler does slow work before responding</td>
<td>Return 200 immediately, then process on a queue. See below.</td>
</tr>
<tr>
<td>Wrong response from the webhook: 404</td>
<td>Path mismatch between registered URL and your route</td>
<td>Compare the <code>url</code> field character by character against your router.</td>
</tr>
</tbody></table>
<h3>Four webhook rules that are easy to violate</h3>
<p>Telegram is strict about webhook endpoints in ways that other platforms are not:</p>
<ul>
<li><strong>HTTPS only.</strong> A plain HTTP URL is rejected at registration time. There is no development exception.</li>
<li><strong>Only four ports.</strong> 443, 80, 88 and 8443. A webhook on port 3000 will never be called, and Telegram will not warn you clearly about why - the registration error is <a href="https://www.conferbot.com/errors/telegram/400-bad-request-bad-webhook-webhook-can-be-set-up-only-on-ports-80-88-443-or-8443">Bad webhook: webhook can be set up only on ports 80, 88, 443 or 8443</a>.</li>
<li><strong>Self-signed certificates require an upload.</strong> You must supply the public certificate with the <code>setWebhook</code> call. A self-signed cert without the upload fails every delivery.</li>
<li><strong>Respond fast, and respond 200.</strong> Any non-2xx counts as a failure and triggers a retry with backoff. Retries mean duplicate processing, which is why users sometimes receive the same reply three times.</li>
</ul>
<h3>The pattern that prevents timeout failures</h3>
<p>The single most valuable structural change you can make is to stop doing work inside the webhook handler. Acknowledge, then process:</p>
<pre><code>app.post('/telegram-webhook', (req, res) =&gt; {
  res.sendStatus(200);        // acknowledge first, always
  queue.push(req.body);       // then do the slow part off the request path
});
</code></pre>
<p>A handler that calls an LLM, queries a database and hits a CRM before responding will exceed Telegram's patience under load. Telegram retries, your bot double-replies, and the failure looks intermittent and impossible to reproduce - because it only happens when the downstream call is slow.</p>
<h3>The four-port rule and the file-size limit both trace back to one thing</h3>
<p>The port restriction (443, 80, 88, 8443) and the fact that a bot can only send files up to 50MB and download files up to 20MB both come from the same source: by default your bot talks to Telegram's shared cloud Bot API server, and that server enforces both limits for every bot on it. Telegram also publishes the Bot API server itself as software you can run yourself. A self-hosted local Bot API server lifts the port restriction entirely - webhooks can use any port - and raises the file-size ceiling to roughly 2000MB in each direction. It is a meaningful operational step up from calling <code>api.telegram.org</code> directly, and it is only worth taking if large media or the port restriction is an actual blocker, not a default choice.</p>
<table>
<thead>
<tr>
<th>Limit</th>
<th>Cloud Bot API (default)</th>
<th>Self-hosted local server</th>
</tr>
</thead>
<tbody><tr>
<td>Webhook ports</td>
<td>443, 80, 88, 8443 only</td>
<td>Any port</td>
</tr>
<tr>
<td>File upload</td>
<td>Up to 50MB</td>
<td>Up to roughly 2000MB</td>
</tr>
<tr>
<td>File download</td>
<td>Up to 20MB</td>
<td>Up to roughly 2000MB</td>
</tr>
</tbody></table>
<p>Everything in this section - unreachable hosts, certificate chains, slow handlers, the raw-body signature trap - has an equivalent on Meta, Slack and Discord; our <a href="https://www.conferbot.com/blog/webhook-not-firing-debugging-guide">webhook not firing debugging guide</a> covers the shared causes and the one debug order that finds them.</p>
<h2>4. 403 Forbidden: the user blocked your bot, or never started it</h2>
<p>Two 403 responses look similar and mean different things:</p>
<pre><code>{"ok":false,"error_code":403,"description":"Forbidden: bot was blocked by the user"}
{"ok":false,"error_code":403,"description":"Forbidden: bot can't initiate conversation with a user"}
</code></pre>
<p>The first is exactly what it says. The user blocked you. There is no recovery path and no API to check block status in advance - the send attempt <em>is</em> the check. The correct response is to mark that chat inactive in your database and stop sending (the <a href="https://www.conferbot.com/errors/telegram/403-forbidden-bot-was-blocked-by-the-user">bot was blocked by the user</a> page has the handling pattern). Bots that keep retrying blocked users burn rate limit on messages that will never arrive.</p>
<p>The second is a rule people meet the first time they try to build a notification system: <strong>a Telegram bot can never message someone first.</strong> The user must initiate by sending <code>/start</code>, or by tapping a deep link. If you have a phone number or a user ID from elsewhere, that is not enough - Telegram requires the user to opt in inside Telegram itself.</p>
<p>The standard pattern is a deep link that carries your own identifier:</p>
<pre><code>https://t.me/YourBotName?start=user_12345
</code></pre>
<p>When the user taps it, your bot receives <code>/start user_12345</code>, and you can map their Telegram chat ID to your internal user record. From that moment you are allowed to send. This is the same opt-in model most platforms use, and it is why a Telegram notification flow always begins with a link rather than a phone number. Hand-building these links is easy to get wrong - a stray space or an unencoded character breaks the parameter silently - so a small <a href="https://www.conferbot.com/tools/telegram-link-generator">Telegram link generator</a> that produces the correctly formatted <code>?start=</code> URL for you is worth using rather than concatenating strings by hand, especially once the link is going into a QR code or an email template where you cannot easily eyeball it.</p>
<h2>5. Rate limits: the bot that works in testing and dies at launch</h2>
<p>A bot that answered every message during development and started dropping replies the day it launched is usually hitting <a href="https://www.conferbot.com/glossary/term/rate-limiting">rate limits</a>, not bugs. Telegram's published guidance for bots (the full set of ceilings is on the <a href="https://www.conferbot.com/limits/telegram">Telegram limits page</a>):</p>
<table>
<thead>
<tr>
<th>Scope</th>
<th>Limit</th>
<th>What breaks first</th>
</tr>
</thead>
<tbody><tr>
<td>Overall outbound</td>
<td>~30 messages/second</td>
<td>Broadcasts and announcement sends</td>
</tr>
<tr>
<td>Per individual chat</td>
<td>~1 message/second sustained</td>
<td>Multi-bubble replies sent in a burst</td>
</tr>
<tr>
<td>Per group</td>
<td>~20 messages/minute</td>
<td>Community bots during busy periods</td>
</tr>
</tbody></table>
<p>When you exceed a limit the API returns 429 with a <code>retry_after</code> value in seconds:</p>
<pre><code>{"ok":false,"error_code":429,"description":"Too Many Requests: retry after 34","parameters":{"retry_after":34}}
</code></pre>
<p>Honour <code>retry_after</code> exactly - the <a href="https://www.conferbot.com/errors/telegram/429-too-many-requests-retry-after">429 reference page</a> covers the back-off rules. Retrying sooner extends the penalty, and sustained abuse can get a token limited for far longer than the number suggests. The fix for broadcasts is a token-bucket queue that paces sends below 30/second rather than a loop that fires as fast as the network allows.</p>
<p>A subtler version affects conversational bots: sending three quick bubbles to feel human - a greeting, then a question, then a menu - can trip the per-chat limit for that user. Insert a short delay between bubbles, which is also better conversation design, since three messages arriving in the same instant reads as a machine rather than a person.</p>
<p>Telegram also offers a paid path past the 30/second ceiling for bots that need genuine bulk broadcast volume: enabling paid broadcasts through BotFather raises the outbound cap to roughly 1,000 messages per second, billed per message above the free 30/second tier in Telegram Stars. That is a broadcast-scale feature, not something a conversational support bot needs - if you are hitting 30/second in normal use, the fix is almost always pacing or batching, not paying for more headroom.</p>
<h2>6. Token problems, and the deploy that quietly used the wrong one</h2>
<p>A malformed or revoked token returns:</p>
<pre><code>{"ok":false,"error_code":401,"description":"Unauthorized"}
</code></pre>
<p>Verify the token in isolation before suspecting anything else - our <a href="https://www.conferbot.com/tools/telegram-bot-token-checker">Telegram bot token checker</a> does the same <code>getMe</code> call from the browser:</p>
<pre><code>https://api.telegram.org/bot&lt;TOKEN&gt;/getMe
</code></pre>
<p>A healthy token returns the bot's own username and ID. If <code>getMe</code> works from your laptop but the deployed bot returns 401, the token is fine and your environment variable is not - it is missing, truncated, or shadowed by another value.</p>
<p>Three failure modes account for most of these:</p>
<ul>
<li><strong>A trailing newline.</strong> Tokens pasted into a secrets manager or a <code>.env</code> file often carry <code>\n</code>. Trim the value before use; the API rejects it and the error gives no hint about whitespace.</li>
<li><strong>Quotes included.</strong> <code>TELEGRAM_TOKEN="123:ABC"</code> in some shell and container setups produces a token that literally contains quote characters.</li>
<li><strong>The staging token in production.</strong> If you built a second bot for testing, both tokens are valid and both pass <code>getMe</code>. The bot responds - just not the one anyone is looking at. If users report silence while your logs show successful sends, check which bot the token belongs to.</li>
</ul>
<p>Note also that regenerating a token via BotFather immediately invalidates the old one. If a bot stopped working the moment someone "rotated the key for security", that is your answer.</p>
<h2>7. The bot sends nothing because the message itself is invalid</h2>
<p>Some "not responding" reports are really "responding with an error you are not logging". The most common is a parse failure:</p>
<pre><code>{"ok":false,"error_code":400,"description":"Bad Request: can't parse entities: Can't find end of the entity starting at byte offset 42"}
</code></pre>
<p>This happens when you set <code>parse_mode</code> to <code>MarkdownV2</code> or <code>HTML</code> and the message body contains characters the parser treats as markup. MarkdownV2 is particularly unforgiving: it requires escaping for <code>_ * [ ] ( ) ~ ` &gt; # + - = | { } . !</code>, including inside ordinary prose. A perfectly innocent sentence ending in a full stop will fail.</p>
<p>This is the classic cause of a bot that handles most messages and dies on specific ones - a customer whose name contains an underscore, a product code with a hyphen, an address with brackets. It looks intermittent; it is entirely deterministic.</p>
<p>Three practical defences:</p>
<ul>
<li>Escape all interpolated user content before inserting it into a formatted message. Never trust that a name or a search term is markup-safe.</li>
<li>Prefer <code>HTML</code> parse mode over <code>MarkdownV2</code>. It needs only three escapes - <code>&amp;</code>, <code>&lt;</code>, <code>&gt;</code> - which is far easier to get right.</li>
<li>Log the full API response body, not just the status code. Telegram tells you the exact byte offset that failed, and that detail is lost if you only log "send failed".</li>
</ul>
<p>The related error <a href="https://www.conferbot.com/errors/telegram/400-bad-request-message-is-too-long"><code>Bad Request: message is too long</code></a> fires at 4,096 characters. AI-generated replies overrun this regularly, so split long responses on a paragraph boundary before sending rather than discovering the limit in production.</p>
<h2>8. Chat not found, and other identifier mistakes</h2>
<p>{"ok":false,"error_code":400,"description":"Bad Request: chat not found"}</p>
<p>This means the <code>chat_id</code> you sent does not correspond to a chat your bot can reach. In order of likelihood:</p>
<ul>
<li><strong>You used a username where an ID was needed.</strong> A bot can address a public channel as <code>@channelname</code>, but private chats require the numeric ID.</li>
<li><strong>The group was upgraded to a supergroup.</strong> This changes the chat ID, and the old one stops working permanently. Telegram sends a <code>migrate_to_chat_id</code> field in an update when it happens - handle it and store the new ID, or your bot silently loses the group.</li>
<li><strong>Negative sign dropped.</strong> Group and channel IDs are negative, and supergroup IDs begin with <code>-100</code>. Storing a chat ID in a column that strips the sign, or as an unsigned integer, produces exactly this error.</li>
<li><strong>The bot was removed from the group</strong> and nobody updated the record.</li>
</ul>
<p>A related trap: chat IDs can exceed 32 bits. Storing them in a 32-bit integer column truncates the value and produces "chat not found" for a subset of users - typically the newest ones, since IDs increase over time. Use a 64-bit integer or a string.</p>
<h2>9. The bot responds, but users cannot find how to talk to it</h2>
<p>A final category is not technically a failure. The bot works; nobody triggers it.</p>
<p>If you never called <code>setMyCommands</code>, Telegram's menu button shows nothing, and users are left guessing what to type. Registering commands makes them appear in the slash menu and in the blue menu button next to the input field:</p>
<pre><code>https://api.telegram.org/bot&lt;TOKEN&gt;/setMyCommands?commands=[{"command":"start","description":"Start the bot"},{"command":"help","description":"Show what I can do"}]
</code></pre>
<p>Two related details matter for real usage:</p>
<ul>
<li><strong>Clients cache the command list.</strong> After updating commands, the change can take a while to appear for users who already have the chat open. Test in a fresh client before concluding the call failed.</li>
<li><strong>Only <code>/start</code> is guaranteed.</strong> It is the one command every Telegram user knows to try, and it is what the deep link sends. Whatever else you build, <code>/start</code> should return a clear description of what the bot does and what to say next.</li>
</ul>
<p>If your bot is genuinely conversational rather than command-driven, say so in the <code>/start</code> reply. Users default to hunting for commands, and a bot that accepts natural language but never says so gets far fewer conversations than it should.</p>
<h2>The 5-minute diagnostic, in order</h2>
<p>Run these in sequence. Most bots are fixed before step 4.</p>
<table>
<thead>
<tr>
<th>#</th>
<th>Check</th>
<th>Verdict if it fails</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td><code>getMe</code> returns your bot</td>
<td>Token is wrong, revoked, or has whitespace</td>
</tr>
<tr>
<td>2</td>
<td><code>getWebhookInfo</code> <code>url</code> matches your intent</td>
<td>A stale webhook is stealing updates - <code>deleteWebhook</code></td>
</tr>
<tr>
<td>3</td>
<td><code>last_error_message</code> is empty</td>
<td>Endpoint is rejecting deliveries - read the message, it is accurate</td>
</tr>
<tr>
<td>4</td>
<td><code>pending_update_count</code> is near zero</td>
<td>Nothing is draining the queue - no consumer running</td>
</tr>
<tr>
<td>5</td>
<td>Works in DM but not group</td>
<td>Privacy mode - disable it, then re-add the bot</td>
</tr>
<tr>
<td>6</td>
<td>Only one process holds the token</td>
<td>Two pollers - expect ~50% of messages to vanish</td>
</tr>
<tr>
<td>7</td>
<td>Full API error bodies are logged</td>
<td>You are hiding the answer from yourself</td>
</tr>
</tbody></table>
<p>That last row deserves emphasis. A large share of "my bot mysteriously stopped responding" reports resolve the moment someone logs the response body instead of the status code. Telegram's error descriptions are unusually specific - they name the byte offset, the seconds to wait, the method to call. Code that catches an exception and logs <code>"send failed"</code> throws away the fix.</p>
<h2>Where Telegram earns its place as a business channel</h2>
<p>Telegram's low friction - no app review, instant deep links, a bot that can be built and deployed in an afternoon - is exactly what makes it attractive past the hobby-project stage, and exactly why the failure modes above start to cost more once real customers depend on the bot. Telegram shows up as a genuine support and sales channel most often in <a href="https://www.conferbot.com/chatbot/telegram/ecommerce">ecommerce</a>, for order updates and abandoned-cart recovery where a deep link replaces an app install; <a href="https://www.conferbot.com/chatbot/telegram/logistics">logistics</a>, where drivers and dispatchers already live in Telegram in many regions; <a href="https://www.conferbot.com/chatbot/telegram/education">education</a>, running cohort announcements and Q&amp;A alongside a course; <a href="https://www.conferbot.com/chatbot/telegram/media">media and publishing</a>, distributing content and running reader communities; <a href="https://www.conferbot.com/chatbot/telegram/nonprofit">nonprofits</a>, coordinating volunteers at low cost; and <a href="https://www.conferbot.com/chatbot/telegram/tech">tech companies</a>, where it is often the default support channel for a technical audience. Our longer guides to <a href="https://www.conferbot.com/blog/telegram-bot-for-business-guide">Telegram for business</a> and <a href="https://www.conferbot.com/blog/telegram-bot-customer-support">Telegram bots for customer support</a> cover the conversation and workflow side once the transport layer is no longer the problem.</p>
<h2>Avoiding this class of problem entirely</h2>
<p>Every cause above comes from operating the Bot <a href="https://www.conferbot.com/glossary/term/api-endpoint">API</a> directly: you own the <a href="https://www.conferbot.com/glossary/term/webhook">webhook</a> lifecycle, the polling process, the retry semantics, the <a href="https://www.conferbot.com/glossary/term/rate-limiting">rate limiting</a>, the escaping and the opt-in mapping. That is entirely reasonable for a hobby bot or a team with backend capacity to spare.</p>
<p>It is a poor trade when the bot is a support or sales channel for a business, because none of that work is differentiating. The webhook conflict, the parse-mode escaping and the token-bucket queue are the same problems for every bot on the platform - and every other chat platform makes you pay a version of the same tax under a different name, as our guides to <a href="https://www.conferbot.com/blog/discord-bot-not-responding">Discord bots not responding</a>, <a href="https://www.conferbot.com/blog/slack-bot-not-responding">Slack bots not responding</a> and <a href="https://www.conferbot.com/blog/chat-widget-not-showing-on-website">chat widgets not showing on a website</a> cover for their respective transports, and <a href="https://www.conferbot.com/blog/chatbot-api-rate-limiting">chatbot API rate limiting</a> covers in general.</p>
<p>A managed platform handles the transport layer so the failure modes in this guide never reach you. With <a href="https://www.conferbot.com/chatbot/telegram">Conferbot's Telegram integration</a>, available from the Pro plan up, you connect a bot token and build the conversation in a visual flow editor - webhook registration, retries, rate limiting and message formatting are handled for you. The same flow deploys to <a href="https://www.conferbot.com/chatbot/whatsapp">WhatsApp</a>, <a href="https://www.conferbot.com/chatbot/discord">Discord</a>, <a href="https://www.conferbot.com/chatbot/slack">Slack</a> and a <a href="https://www.conferbot.com/chatbot/website">website widget</a> without rewriting it per platform, and message formatting adapts to each one's capabilities automatically. Conversations can hand data off through Conferbot's built-in <a href="https://www.conferbot.com/features/integrations-hub">integrations</a> - Webhook, Zapier, Slack, Mailchimp, Stripe, SalesForce, HubSpot, Google Sheets, Airtable, Calendly and Google Calendar - or through the <a href="https://www.conferbot.com/features/chatbot-api-integration">API</a> for anything more custom.</p>
<p><a href="https://app.conferbot.com"><strong>Start free with Conferbot</strong></a> - the free plan includes 600 conversations a month and needs no credit card, so you can have the same flow running on a second channel this afternoon.</p>
<p>If you are debugging a Telegram bot today, work the diagnostic table above first - the answer is usually in <code>getWebhookInfo</code>. If you find yourself rebuilding the same transport plumbing for a third channel, that is the point where a platform starts paying for itself.</p>
<h2>FAQ</h2>
<p><strong>Why is my Telegram bot online but not responding to messages?</strong></p>
<p>A bot showing online only means its token is valid - it says nothing about whether updates are reaching your code. The most common cause is a webhook registered against the token while your code polls with getUpdates, which returns a 409 conflict that many libraries silently retry. Call getWebhookInfo: if the url field is populated but you expect polling, run deleteWebhook. If pending_update_count is large and climbing, nothing is consuming updates at all.</p>
<p><strong>How do I fix "Conflict: can't use getUpdates method while webhook is active"?</strong></p>
<p>Call <a href="https://api.telegram.org/bot/deleteWebhook?drop_pending_updates=true">https://api.telegram.org/bot/deleteWebhook?drop_pending_updates=true</a> and restart your bot. Telegram allows only one update consumer per token, so a registered webhook blocks polling entirely. Include drop_pending_updates=true unless you want every message queued while the bot was down to replay at once.</p>
<p><strong>Why does my Telegram bot work in private chat but not in groups?</strong></p>
<p>Privacy mode. By default a bot in a group only receives slash commands, @-mentions, replies to its own messages, and service messages - ordinary conversation is never delivered. Disable it with /setprivacy in @BotFather, then remove the bot from the group and re-add it, because the change does not apply to groups it has already joined. Promoting the bot to group admin achieves the same visibility.</p>
<p><strong>What does "Forbidden: bot can't initiate conversation with a user" mean?</strong></p>
<p>A Telegram bot can never message someone first. The user must send /start or tap a deep link such as <a href="https://t.me/YourBotName?start=user_12345">https://t.me/YourBotName?start=user_12345</a> before your bot is permitted to send anything. Having their phone number or user ID from another system does not grant permission - the opt-in has to happen inside Telegram.</p>
<p><strong>Why does my bot only respond to about half the messages?</strong></p>
<p>Two processes are polling the same token. Each long-poll connection claims updates exclusively, so messages are split unpredictably between the instances. Check for a local dev process still running, a container scaled beyond one replica, or an old deploy that was never stopped. Polling cannot be scaled horizontally - use webhooks if you need multiple instances.</p>
<p><strong>What ports can a Telegram webhook use?</strong></p>
<p>Only 443, 80, 88 and 8443, and the URL must be HTTPS. A webhook on any other port - port 3000 being the usual mistake - will never receive a delivery. Self-signed certificates additionally require uploading the public certificate with the setWebhook call.</p>
<p><strong>What are Telegram bot rate limits?</strong></p>
<p>Roughly 30 messages per second overall, about 1 message per second sustained to an individual chat, and around 20 messages per minute to a single group. Exceeding a limit returns HTTP 429 with a retry_after value in seconds, which you should honour exactly - retrying sooner extends the penalty.</p>
<p><strong>Why do I get "can't parse entities" when sending a message?</strong></p>
<p>Your message contains characters that the chosen parse_mode treats as markup. MarkdownV2 requires escaping for _ * [ ] ( ) ~ ` &gt; # + - = | { } . ! even in ordinary prose, so a sentence ending in a full stop will fail. Escape interpolated user content, or use HTML parse mode which needs only &amp;, &lt; and &gt; escaped.</p>
<p><strong>Why does my bot suddenly get "chat not found" for a group that worked before?</strong></p>
<p>The group was almost certainly upgraded to a supergroup, which permanently changes its chat ID. Telegram sends a migrate_to_chat_id field in an update when this happens - store the new ID when you see it. Also confirm you are preserving the negative sign and using a 64-bit integer, since group IDs are negative and can exceed 32 bits.</p>
<p><strong>How do I check whether my Telegram bot token is valid?</strong></p>
<p>Open <a href="https://api.telegram.org/bot/getMe">https://api.telegram.org/bot/getMe</a> in a browser. A valid token returns the bot's username and ID; an invalid one returns 401 Unauthorized. If this works locally but the deployed bot returns 401, the token is fine and your environment variable is wrong - usually a trailing newline, included quote characters, or the staging token deployed to production.</p>
<p><strong>Why is my bot sending the same reply multiple times?</strong></p>
<p>Your webhook handler is too slow, so Telegram times out and retries the delivery, and your code processes the same update more than once. Return HTTP 200 immediately and push the work onto a queue rather than calling an LLM or database before responding. Deduplicating on update_id gives you a second layer of protection.</p>
<p><strong>Is there a limit to Telegram message length?</strong></p>
<p>Yes, 4,096 characters per message. Exceeding it returns "Bad Request: message is too long" and nothing is delivered. AI-generated replies overrun this regularly, so split long output on a paragraph boundary before sending.</p>
<hr />
<p>The full guide, with diagrams and the latest checks, is at <a href="https://www.conferbot.com/blog/telegram-bot-not-responding">https://www.conferbot.com/blog/telegram-bot-not-responding</a>.</p>
]]></content:encoded></item></channel></rss>