Why Slack bots fail silently, and where to look first
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.
Last verified: August 2026 against the Slack Events API and OAuth scopes model.
Before changing code, answer one question: is your endpoint receiving anything at all? 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.
| Observation | Meaning | Section |
|---|---|---|
| Nothing logged at all | Slack is not sending - subscription, verification or invite problem | 1, 2, 4 |
| Request logged, no reply appears | Missing scope, or the send is failing | 3 |
| Reply appears several times | Retry storm from a slow handler | 5 |
| Works in one channel only | Bot not invited elsewhere | 4 |
That single log line eliminates roughly half the possible causes in under a minute.
1. URL verification: "Your URL didn't respond with the value of the challenge parameter"
Before Slack will deliver any event, it verifies you own the endpoint. This is the same push shape as any incoming webhook: Slack POSTs a JSON body:
{
"token": "Jhj5dZrVaK7ZwHHjRyZWjbDl",
"challenge": "3eZbrw1aB1m5zVFQrRvBiVAG1MMqbGqBGRTLBSFwhcSFrSKzHJ",
"type": "url_verification"
}You must respond with the challenge value and nothing else. This must happen before any signature verification, authentication middleware or body parsing that might reject an unfamiliar payload:
app.post('/slack/events', (req, res) => {
if (req.body.type === 'url_verification') {
return res.send(req.body.challenge); // plain text, first thing
}
// ... normal event handling below
});Five reasons this fails
- Signature verification runs first. Your middleware rejects the verification request because it does not match an expected shape. Handle
url_verificationbefore any guard. - You returned JSON instead of the raw string. Returning
{"challenge": "..."}also works if the content type is correct, but returning the plain string is more reliable. - A redirect sits in front. 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.
- Authentication is required. Any auth layer, IP allowlist or WAF rule in front of the endpoint blocks Slack. The events path must be publicly reachable.
- Cold start timeout. 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.
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, Socket Mode removes the public endpoint requirement entirely and is the better development path.
2. Scopes: the reinstall step everyone forgets
Slack's most common runtime error:
{"ok": false, "error": "missing_scope", "needed": "chat:write", "provided": "channels:read,users:read"}Helpfully, the response names exactly what you need. The trap is what comes next.
Adding a scope in the app configuration does nothing until the app is reinstalled to the workspace. The token you already hold was issued with the old scope set and does not gain new permissions retroactively. Change scopes, then go to OAuth & Permissions -> Reinstall to Workspace, and use the newly issued token. Skipping the reinstall is the single most common Slack integration mistake.
The scopes a conversational bot actually needs
| Scope | Grants |
|---|---|
chat:write | Send messages to channels the bot is in |
app_mentions:read | Receive @-mentions of the bot |
im:history, im:write | Read and send direct messages |
channels:history | Read messages in public channels the bot is in |
groups:history | Read messages in private channels - separate from the above |
users:read | Resolve user IDs to names |
chat:write.public | Post to public channels without being invited |
Two distinctions matter. Public and private channel history are separate scopes, so a bot that reads public channels fine and sees nothing in private ones needs groups:history. And bot tokens (xoxb-) differ from user tokens (xoxp-): most bot actions need the bot token, and using the wrong one produces confusing permission errors.
App manifests remove the drift between environments
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 App Manifest 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 App Manifest 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 oauth_config.scopes still requires reinstalling to take effect - but it does remove the class of bug where two environments quietly diverge.
3. Events not firing at all
If your endpoint verified successfully but no events arrive, work these in order.
You subscribed to no events
Verifying the URL and subscribing to events are separate steps. Under Event Subscriptions -> Subscribe to bot events, add the specific events you need - most commonly message.channels, message.im, message.groups and app_mention. An empty subscription list means Slack has nothing to send.
You subscribed to the wrong message event
Message events are split by conversation type, and each is a distinct subscription:
message.channels- public channelsmessage.groups- private channelsmessage.im- direct messagesmessage.mpim- group direct messages
A bot that responds in public channels and ignores DMs is subscribed to message.channels but not message.im. This is the Slack equivalent of the intent split in other platforms.
The bot is not in the channel
Subscribing to message.channels does not deliver messages from channels the bot has not joined. Invite it with /invite @yourbot in each channel where it should work. A bot that works in one channel and not another has almost always just not been invited.
Attempting to post to a channel it is not in returns:
{"ok": false, "error": "not_in_channel"}The chat:write.public scope allows posting to public channels without an invite, but it does not grant the ability to read them - you still need the bot in the channel to receive messages.
Your app is on the wrong delivery mode
Socket Mode and the Events API are mutually exclusive. If Socket Mode is enabled, Slack pushes over a WebSocket and ignores your request URL entirely - 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 xapp-, generated from the app's Basic Information page with the connections:write scope, which is easy to skip if you only ever set up a bot token before.
4. The 3-second rule and the retry storm
Slack requires an HTTP 200 within 3 seconds of delivering an event. 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.
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.
Acknowledge first, then work:
app.post('/slack/events', async (req, res) => {
res.sendStatus(200); // acknowledge immediately, always
await handleEvent(req.body); // then take as long as you need
});For slash commands and interactive components you can also respond immediately with a placeholder and update it later using the response_url, which stays valid for 30 minutes and accepts up to five follow-up messages.
Detect retries rather than reprocessing them
Slack marks retried deliveries with headers you should check:
X-Slack-Retry-Num: 1
X-Slack-Retry-Reason: http_timeoutX-Slack-Retry-Reason is worth logging on its own - besides http_timeout it can read connection_failed, ssl_error, http_error or too_many_redirects, and each points somewhere different: a timeout means your handler is too slow, but ssl_error or too_many_redirects means the retry never had a chance to reach your code regardless of how fast it responds. If X-Slack-Retry-Num is present at all, acknowledge and return without reprocessing. Combine that with deduplication on event_id and duplicate replies disappear entirely - even if a genuine timeout occurs.
5. Infinite loops, thread targeting and ID mistakes
The bot replying to itself
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:
if (event.bot_id || event.subtype === 'bot_message') return;Two unguarded bots in one channel will do this to each other and produce thousands of messages in seconds.
Message subtypes you probably want to ignore
Edits, deletions, joins and file shares all arrive as message events with a subtype. A bot that responds to message_changed will reply again every time someone edits a message. Handle only the subtypes you intend to.
Replying in the wrong place
To reply inside a thread, pass thread_ts - the ts of the parent message. Omit it and your reply lands in the channel, out of context. Conversely, passing thread_ts when you meant to post to the channel buries the message in a thread nobody is watching.
channel_not_found
Usually a channel name used where an ID was required. Slack channel IDs look like C01ABCDEF; using #general 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.
Rate limits
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 - 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 users.info once per incoming message instead of caching the result, will get throttled long before it comes anywhere near the chat.postMessage 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.
The diagnostic checklist
| # | Check | If it fails |
|---|---|---|
| 1 | Request URL shows Verified | Handle url_verification before any middleware |
| 2 | Socket Mode is OFF in production | It overrides your request URL completely |
| 3 | Bot events are subscribed, not just the URL verified | Add message.im, message.channels, app_mention |
| 4 | App reinstalled since the last scope change | Old tokens never gain new scopes |
| 5 | Bot invited to the channel | /invite @yourbot |
| 6 | Handler returns 200 within 3 seconds | Acknowledge first, process after |
| 7 | Bot messages ignored | Guard on bot_id to prevent loops |
| 8 | Full API error bodies logged | Slack names the missing scope for you |
Row 8 deserves emphasis. Slack's missing_scope response includes both needed and provided, which is as close to a self-diagnosing error as any platform offers. Code that logs only a status code discards the answer.
6. Buttons, modals and slash commands that do nothing
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.
The Interactivity request URL is separate
This is the most common cause and the least obvious. Event Subscriptions and Interactivity & Shortcuts have their own separate request URLs. 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 Interactivity & Shortcuts, turn interactivity on, and set a request URL there too - it can be the same endpoint, but it must be configured.
Interaction payloads are form-encoded, not JSON
Slack sends interactive payloads as application/x-www-form-urlencoded with a single payload field containing JSON as a string. An endpoint that only parses JSON receives nothing usable:
app.post('/slack/interactive', (req, res) => {
res.sendStatus(200); // acknowledge first
const payload = JSON.parse(req.body.payload); // note: not req.body
handleInteraction(payload);
});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.
trigger_id expires in 3 seconds
Opening a modal requires a trigger_id from the interaction payload, and it is valid for 3 seconds only. You cannot defer, queue, or await an AI call before calling views.open - the trigger will be dead. Open the modal immediately with a loading state, then update it with views.update once the slow work finishes.
The error you get is expired_trigger_id, and it appears intermittently under load, which makes it look like a flaky network problem rather than a design constraint.
Slash commands have their own URL too
Each slash command is registered individually under Slash Commands, 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 /help command triggers something unexpected, a competing app owns it.
Block Kit validation failures
Malformed Block Kit JSON returns invalid_blocks 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.
7. Four tokens that look similar and do different jobs
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:
| Prefix | Type | Used for |
|---|---|---|
xoxb- | Bot token | The app's own identity. Almost everything in this guide - chat.postMessage, reading channel history - uses this one. |
xoxp- | User token | Acts as a specific human who authorized it. Wrong token for almost any bot action - using it where xoxb- is expected produces confusing permission errors that look like a scope problem but are not. |
xapp- | App-level token | Required specifically for Socket Mode. Generated from Basic Information, and it needs the connections:write scope or the WebSocket connection will not open at all. |
xwfp- | Workflow token | Short-lived, issued to a workflow step. Expires 15 minutes after issue or the moment the step completes - do not cache it. |
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 xapp- app-level token with connections:write, 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 OAuth & Permissions. Both look, superficially, like a OAuth scope problem, and neither is fixed by adding scopes.
Confirm which token you are actually holding
Rather than guessing from the prefix, call auth.test with the token in question:
curl -X POST https://slack.com/api/auth.test \
-H "Authorization: Bearer YOUR_TOKEN"The response identifies exactly what the token is - the workspace (team), the bot or user it belongs to (user_id, bot_id 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.
8. Workspace installs, org-wide apps and admin approval
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.
App approval is on by default in many organisations
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.
Org-wide installs behave differently from workspace installs
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 team_id 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.
Token storage for multi-workspace apps
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 team_id (and enterprise_id for Grid) and look up the right one per incoming event.
Token rotation
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 token_expired, rotation is enabled and you are not refreshing. Either implement the refresh flow or disable rotation if your security posture allows.
A note on Slack Connect channels
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.
Where Slack shows up as a real support channel
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 tech companies routing product and engineering questions, HR teams fielding policy and benefits questions where our guide to Slack and Teams employee support covers the conversation design side, government agencies running internal case coordination, and manufacturing teams using it for shift and maintenance coordination. If the workflow is IT ticketing specifically, Slack IT support is the more direct read, and Slack chatbots for business covers the broader case for the channel.
Slack is not the only workplace chat platform with this exact list of failure modes - Microsoft Teams has its own version, covered in Teams chatbots for HR, and a business running both ends up maintaining two independent scope-and-webhook lifecycles for the same underlying conversation.
When the plumbing stops being worth it
Everything above is transport work: challenge handshakes, OAuth scope lifecycles, event subscription matrices, acknowledgement deadlines, retry deduplication, rate pacing. It is the same list for every Slack app ever built, and none of it is your product.
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 - and our guides to Telegram bots not responding, Discord bots not responding and chat widgets not showing on a website exist precisely because those lists share nothing with this one. Our guide to chatbot API rate limiting goes deeper on the pacing problem than any single platform's docs will.
Conferbot's Slack integration 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 Microsoft Teams, Telegram, WhatsApp, Discord and a website widget, with formatting adapted per platform. Conversations can push data out through Conferbot's built-in integrations - Webhook, Zapier, Slack, Mailchimp, Stripe, SalesForce, HubSpot, Google Sheets, Airtable, Calendly and Google Calendar - or through the API directly.
Start free with Conferbot - 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.
Was this article helpful?
Build and deploy in 10 minutes. No coding needed.
Slack Bot Not Responding? Fix URL Verification, Missing Scopes and Silent Events FAQ
Everything you need to know about chatbots for slack bot not responding? fix url verification, missing scopes and silent events.
About the Author
The Conferbot team writes about building, deploying, and improving AI chatbots.
View all articles