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. It 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.
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.
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 increasing intervals. 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_timeoutIf X-Slack-Retry-Num is present, 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.
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. 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.
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 and Discord bots not responding exist precisely because those lists share nothing with this one.
Conferbot's Slack integration 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.
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