- Introduction
- 1. Slack App Architecture and Setup
- 2. Installing the Python Bolt SDK and the Basic Setup
- 3. Event Handling -- Messages, Mentions, Reactions
- 4. Implementing Slash Commands
- 5. Modals and Interactive Components
- 6. Rich Messages with Block Kit
- 7. Socket Mode vs HTTP Mode
- 8. Bot Framework Comparison -- Slack Bolt vs discord.py vs Telegram Bot
- 9. Deployment Strategy -- Docker and Kubernetes
- 10. Error Handling and Rate Limit Management
- 11. Failure Cases and Recovery Procedures
- 12. Operational Notes
- 13. Troubleshooting
- 14. Production Deployment Checklist
- Conclusion
- References
Introduction
Slack is a workplace communication platform used by hundreds of thousands of teams around the world. Beyond simple messaging, a Slack Bot lets you automate all sorts of work: incident alerts, deployment automation, data lookups, approval workflows and more.
Bolt for Python, Slack's official framework, lets you implement every Slack interaction - event handling, slash commands, modals, Block Kit - with a concise decorator pattern. This article covers everything you need in practice, from app setup to production deployment, error handling and failure recovery.
1. Slack App Architecture and Setup
1.1 Slack App Architecture Overview
The flow of a Slack Bot works as follows.
- The user performs an interaction in Slack - a message, a slash command, a button click
- The Slack server forwards the event to the bot server (over HTTP or WebSocket)
- The bot handles the event and responds through the Slack Web API
- The result is shown to the user as a message, a modal or a rich block
1.2 Creating the App and Configuring OAuth
# Step 1: create the app on the Slack API site
# go to https://api.slack.com/apps
# "Create New App" -> choose "From scratch"
# App Name: "MySlackBot", choose the workspace
# Step 2: set the Bot Token Scopes under OAuth & Permissions
# the scopes you need:
# chat:write - send messages
# chat:write.public - send messages to public channels the bot has not joined
# commands - register slash commands
# im:history - read DM messages
# im:read - access DM channel information
# channels:history - read public channel messages
# channels:read - list channels and read channel info
# users:read - read user profiles
# reactions:read - read reaction information
# reactions:write - add reactions
# files:write - upload files
# Step 3: enable Event Subscriptions
# Subscribe to bot events:
# message.im - receive DMs
# message.channels - receive channel messages
# app_mention - receive @bot mentions
# app_home_opened - the app home tab is opened
# reaction_added - reaction added event
# reaction_removed - reaction removed event
# Step 4: enable Interactivity & Shortcuts
# required to use interactive features such as modals, buttons and select menus
# Step 5: enable Socket Mode (for development / simple deployment)
# Settings -> Socket Mode -> Enable Socket Mode
# create an App-Level Token (connections:write scope)
A caution: the Bot Token (xoxb-) and the App Token (xapp-) serve different purposes. The Bot Token is used to call the Slack Web API; the App Token is used for the Socket Mode WebSocket connection.
1.3 The Token Types
| Token type | Prefix | Purpose | Where to get it |
|---|---|---|---|
| Bot Token | xoxb- | Slack API calls (messages, channels and so on) | OAuth & Permissions |
| App Token | xapp- | Socket Mode WebSocket connection | Basic Information |
| User Token | xoxp- | API calls made with the user's permissions | OAuth & Permissions |
| Signing Secret | - | Verifying the signature of HTTP requests | Basic Information |
2. Installing the Python Bolt SDK and the Basic Setup
2.1 Project Initialization
# Python 3.9+ recommended
python -m venv .venv
source .venv/bin/activate
# install the core dependencies
pip install slack-bolt==1.27.0 slack-sdk python-dotenv
# project directory structure
mkdir -p slack-bot/{handlers,services,utils}
touch slack-bot/{app.py,.env,requirements.txt}
touch slack-bot/handlers/{__init__.py,commands.py,events.py,actions.py,modals.py}
touch slack-bot/services/{__init__.py,notification.py}
touch slack-bot/utils/{__init__.py,rate_limiter.py,logger.py}
# check the directory structure
# slack-bot/
# ├── app.py # main app entry point
# ├── .env # environment variables (tokens and so on)
# ├── requirements.txt
# ├── Dockerfile
# ├── handlers/
# │ ├── __init__.py
# │ ├── commands.py # slash command handlers
# │ ├── events.py # event handlers (messages, mentions)
# │ ├── actions.py # button/select action handlers
# │ └── modals.py # modal view handlers
# ├── services/
# │ ├── __init__.py
# │ └── notification.py # notification service
# └── utils/
# ├── __init__.py
# ├── rate_limiter.py # rate limit utilities
# └── logger.py # logging configuration
2.2 The Basic Main App Setup
# app.py
import os
import logging
from dotenv import load_dotenv
from slack_bolt import App
from slack_bolt.adapter.socket_mode import SocketModeHandler
load_dotenv()
# logging configuration
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
)
logger = logging.getLogger(__name__)
# initialize the Bolt App
app = App(
token=os.environ["SLACK_BOT_TOKEN"],
signing_secret=os.environ.get("SLACK_SIGNING_SECRET"),
)
# ===== global middleware =====
@app.middleware
def log_request(logger, body, next):
"""Middleware that logs every request"""
logger.info(f"Request type: {body.get('type', 'unknown')}")
next()
# ===== register the event handlers =====
from handlers.events import register_event_handlers
from handlers.commands import register_command_handlers
from handlers.actions import register_action_handlers
from handlers.modals import register_modal_handlers
register_event_handlers(app)
register_command_handlers(app)
register_action_handlers(app)
register_modal_handlers(app)
# ===== global error handler =====
@app.error
def global_error_handler(error, body, logger):
logger.exception(f"Unhandled error: {error}")
logger.debug(f"Request body: {body}")
# ===== run =====
if __name__ == "__main__":
logger.info("Slack Bot starting in Socket Mode...")
handler = SocketModeHandler(app, os.environ["SLACK_APP_TOKEN"])
handler.start()
3. Event Handling -- Messages, Mentions, Reactions
Event handling is the heart of a Slack Bot. The Bolt SDK registers event listeners through a decorator pattern.
3.1 Implementing the Event Handlers
# handlers/events.py
import re
from datetime import datetime
def register_event_handlers(app):
"""Register the event handlers on the app"""
# --- handle app mentions ---
@app.event("app_mention")
def handle_app_mention(event, say, client, logger):
"""Called when the bot is @-mentioned"""
user_id = event["user"]
text = event.get("text", "")
channel = event["channel"]
# strip the bot ID out of the mention text
clean_text = re.sub(r"<@\w+>", "", text).strip()
try:
user_info = client.users_info(user=user_id)
display_name = user_info["user"]["real_name"]
except Exception as e:
logger.error(f"Failed to fetch user info: {e}")
display_name = "user"
if "help" in clean_text.lower():
say(
channel=channel,
blocks=[
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": (
f"Hello {display_name}!\n\n"
"*Available commands:*\n"
"- `/task` - create a task\n"
"- `/status` - check service status\n"
"- `/oncall` - check who is on call\n"
"- Feel free to ask anything by DM!"
),
},
}
],
)
else:
say(
channel=channel,
text=f"{display_name}, I will look into what you said.",
thread_ts=event.get("ts"), # reply in a thread
)
# --- handle DM messages ---
@app.event("message")
def handle_message(event, say, logger):
"""Handle channel/DM message events"""
# ignore the bot's own messages and edit/delete events
if event.get("bot_id") or event.get("subtype"):
return
channel_type = event.get("channel_type", "")
text = event.get("text", "")
user_id = event.get("user", "")
# auto-reply to DMs only
if channel_type == "im":
logger.info(f"DM from {user_id}: {text[:50]}...")
# branch on keywords
if "incident" in text.lower() or "outage" in text.lower():
say(
text="Would you like to file an incident report?",
blocks=[
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "Would you like to file an incident report?",
},
"accessory": {
"type": "button",
"text": {"type": "plain_text", "text": "Report incident"},
"action_id": "open_incident_modal",
"style": "danger",
},
}
],
)
else:
say(f"Got your message. If you need more help, type `/help`.")
# --- handle reaction events ---
@app.event("reaction_added")
def handle_reaction_added(event, client, logger):
"""Handle the reaction_added event (e.g. trigger work from an emoji)"""
reaction = event["reaction"]
item = event["item"]
user_id = event["user"]
logger.info(f"Reaction :{reaction}: added by {user_id}")
# mark the task complete on a :white_check_mark: reaction
if reaction == "white_check_mark":
client.chat_postMessage(
channel=item["channel"],
thread_ts=item["ts"],
text=f"<@{user_id}> marked this task as complete.",
)
# mark the task as being looked at on an :eyes: reaction
elif reaction == "eyes":
client.chat_postMessage(
channel=item["channel"],
thread_ts=item["ts"],
text=f"<@{user_id}> is looking into this.",
)
# --- app home tab ---
@app.event("app_home_opened")
def handle_app_home_opened(client, event, logger):
"""Show the dashboard when the app home tab is opened"""
user_id = event["user"]
now = datetime.now().strftime("%Y-%m-%d %H:%M")
try:
client.views_publish(
user_id=user_id,
view={
"type": "home",
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": "Bot Dashboard",
},
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": (
"*Available features:*\n"
"- `/task` : create and manage tasks\n"
"- `/status` : check service status\n"
"- `/oncall` : look up who is on call\n"
"- DM : free-form Q&A"
),
},
},
{"type": "divider"},
{
"type": "context",
"elements": [
{
"type": "mrkdwn",
"text": f"Last updated: {now}",
}
],
},
],
},
)
except Exception as e:
logger.error(f"Failed to publish home tab: {e}")
Key points:
- Checking
event.get("bot_id")andevent.get("subtype")is essential to prevent an infinite loop. - Specifying
thread_tsmakes the reply go into a thread, which keeps the channel tidy. - Reaction events let you build simple emoji-driven workflows.
4. Implementing Slash Commands
A slash command is how a user invokes a bot feature by typing /command.
4.1 A Basic Command Wired to a Modal
# handlers/commands.py
import json
from datetime import datetime
def register_command_handlers(app):
@app.command("/task")
def handle_task_command(ack, body, client, logger):
"""A slash command that opens the task creation modal"""
# ack() must be called within 3 seconds!
ack()
try:
client.views_open(
trigger_id=body["trigger_id"],
view={
"type": "modal",
"callback_id": "task_create_modal",
"title": {"type": "plain_text", "text": "Create task"},
"submit": {"type": "plain_text", "text": "Create"},
"close": {"type": "plain_text", "text": "Cancel"},
"blocks": [
{
"type": "input",
"block_id": "title_block",
"element": {
"type": "plain_text_input",
"action_id": "title_input",
"placeholder": {
"type": "plain_text",
"text": "Enter the task title",
},
},
"label": {"type": "plain_text", "text": "Title"},
},
{
"type": "input",
"block_id": "priority_block",
"element": {
"type": "static_select",
"action_id": "priority_select",
"options": [
{
"text": {"type": "plain_text", "text": "P1 - Urgent"},
"value": "P1",
},
{
"text": {"type": "plain_text", "text": "P2 - High"},
"value": "P2",
},
{
"text": {"type": "plain_text", "text": "P3 - Normal"},
"value": "P3",
},
{
"text": {"type": "plain_text", "text": "P4 - Low"},
"value": "P4",
},
],
},
"label": {"type": "plain_text", "text": "Priority"},
},
{
"type": "input",
"block_id": "assignee_block",
"element": {
"type": "users_select",
"action_id": "assignee_select",
"placeholder": {
"type": "plain_text",
"text": "Choose an assignee",
},
},
"label": {"type": "plain_text", "text": "Assignee"},
},
{
"type": "input",
"block_id": "desc_block",
"element": {
"type": "plain_text_input",
"action_id": "desc_input",
"multiline": True,
"placeholder": {
"type": "plain_text",
"text": "Describe the task in detail",
},
},
"label": {"type": "plain_text", "text": "Description"},
"optional": True,
},
],
},
)
except Exception as e:
logger.error(f"Failed to open modal: {e}")
@app.command("/status")
def handle_status_command(ack, say, command, logger):
"""A command that reports the service status"""
ack()
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
say(
blocks=[
{
"type": "header",
"text": {"type": "plain_text", "text": "Service Status Dashboard"},
},
{
"type": "section",
"fields": [
{"type": "mrkdwn", "text": "*API Server*\nHealthy (v2.4.1)"},
{"type": "mrkdwn", "text": "*Web Frontend*\nHealthy (v3.1.0)"},
{"type": "mrkdwn", "text": "*Worker*\nDegraded (v1.8.2)"},
{"type": "mrkdwn", "text": "*Database*\nHealthy (CPU 38%)"},
],
},
{"type": "divider"},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {"type": "plain_text", "text": "View details"},
"action_id": "status_detail",
"value": "all",
},
{
"type": "button",
"text": {"type": "plain_text", "text": "Refresh"},
"action_id": "status_refresh",
},
],
},
{
"type": "context",
"elements": [
{
"type": "mrkdwn",
"text": f"Queried at: {now}",
}
],
},
],
)
@app.command("/oncall")
def handle_oncall_command(ack, say, command, logger):
"""A command that reports who is currently on call"""
ack()
# in reality this would integrate with an external service such as PagerDuty or OpsGenie
say(
blocks=[
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": (
"*Currently on call*\n\n"
"- Backend: <@U12345678>\n"
"- Frontend: <@U23456789>\n"
"- Infra: <@U34567890>\n\n"
"For anything urgent, DM the person on call."
),
},
}
],
)
ack() must be called: Slack expects an acknowledge response within 3 seconds of sending a slash command. If you do not call ack(), the user is shown an error saying the slash command could not be processed.
5. Modals and Interactive Components
5.1 Handling a Modal Submission
# handlers/modals.py
from datetime import datetime
def register_modal_handlers(app):
@app.view("task_create_modal")
def handle_task_modal_submission(ack, body, client, view, logger):
"""Handle the task creation modal submission"""
# validate the input
values = view["state"]["values"]
title = values["title_block"]["title_input"]["value"]
errors = {}
if len(title) < 3:
errors["title_block"] = "The title must be at least 3 characters."
if errors:
ack(response_action="errors", errors=errors)
return
ack()
# extract the values
priority = values["priority_block"]["priority_select"]["selected_option"]["value"]
assignee = values["assignee_block"]["assignee_select"]["selected_user"]
description = values["desc_block"]["desc_input"].get("value", "No description")
creator = body["user"]["id"]
# send the task-created message to the notification channel
now = datetime.now().strftime("%Y-%m-%d %H:%M")
client.chat_postMessage(
channel="#tasks",
blocks=[
{
"type": "header",
"text": {"type": "plain_text", "text": "A new task has been created"},
},
{
"type": "section",
"fields": [
{"type": "mrkdwn", "text": f"*Title:*\n{title}"},
{"type": "mrkdwn", "text": f"*Priority:*\n{priority}"},
{"type": "mrkdwn", "text": f"*Assignee:*\n<@{assignee}>"},
{"type": "mrkdwn", "text": f"*Created by:*\n<@{creator}>"},
],
},
{
"type": "section",
"text": {"type": "mrkdwn", "text": f"*Description:*\n{description}"},
},
{"type": "divider"},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {"type": "plain_text", "text": "Start task"},
"style": "primary",
"action_id": "task_start",
"value": title,
},
{
"type": "button",
"text": {"type": "plain_text", "text": "Complete"},
"action_id": "task_complete",
"value": title,
},
],
},
{
"type": "context",
"elements": [
{"type": "mrkdwn", "text": f"Created at: {now}"}
],
},
],
)
# DM the assignee
client.chat_postMessage(
channel=assignee,
text=f"A new task has been assigned to you: *{title}* (priority: {priority})\nCreated by: <@{creator}>",
)
5.2 Handling Button Actions
# handlers/actions.py
def register_action_handlers(app):
@app.action("task_start")
def handle_task_start(ack, body, client, action, logger):
"""Handle a click on the Start task button"""
ack()
user_id = body["user"]["id"]
task_title = action["value"]
# update the original message
client.chat_update(
channel=body["channel"]["id"],
ts=body["message"]["ts"],
blocks=[
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": (
f"*{task_title}*\n"
f"Status: in progress\n"
f"Owner: <@{user_id}>"
),
},
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {"type": "plain_text", "text": "Mark complete"},
"style": "primary",
"action_id": "task_complete",
"value": task_title,
},
],
},
],
)
@app.action("task_complete")
def handle_task_complete(ack, body, client, action, logger):
"""Handle a click on the Complete button"""
ack()
user_id = body["user"]["id"]
task_title = action["value"]
client.chat_update(
channel=body["channel"]["id"],
ts=body["message"]["ts"],
blocks=[
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": (
f"~{task_title}~\n"
f"Status: complete\n"
f"Completed by: <@{user_id}>"
),
},
},
],
)
@app.action("open_incident_modal")
def handle_open_incident(ack, body, client, logger):
"""Open the incident report modal"""
ack()
client.views_open(
trigger_id=body["trigger_id"],
view={
"type": "modal",
"callback_id": "incident_report_modal",
"title": {"type": "plain_text", "text": "Report incident"},
"submit": {"type": "plain_text", "text": "Report"},
"blocks": [
{
"type": "input",
"block_id": "severity_block",
"element": {
"type": "static_select",
"action_id": "severity_select",
"options": [
{
"text": {"type": "plain_text", "text": "P1 - Critical"},
"value": "P1",
},
{
"text": {"type": "plain_text", "text": "P2 - Major"},
"value": "P2",
},
{
"text": {"type": "plain_text", "text": "P3 - Minor"},
"value": "P3",
},
],
},
"label": {"type": "plain_text", "text": "Severity"},
},
{
"type": "input",
"block_id": "incident_desc_block",
"element": {
"type": "plain_text_input",
"action_id": "incident_desc_input",
"multiline": True,
"placeholder": {
"type": "plain_text",
"text": "Describe the incident in detail",
},
},
"label": {"type": "plain_text", "text": "Description of the situation"},
},
],
},
)
@app.action("status_refresh")
def handle_status_refresh(ack, body, logger):
"""The status refresh button"""
ack()
logger.info("Status refresh requested")
@app.action("status_detail")
def handle_status_detail(ack, body, logger):
"""The status details button"""
ack()
logger.info("Status detail requested")
6. Rich Messages with Block Kit
Block Kit is Slack's UI framework for composing structured, interactive messages. The main block types are as follows.
| Block type | Purpose | Where it is used |
|---|---|---|
section | Text, fields, an accessory (a button and so on) | Messages, modals |
actions | Buttons, select menus, date pickers | Messages |
input | User input (text, select) | Modals only |
header | A large text heading | Messages, modals |
divider | A divider line | Messages, modals |
context | Small text, images | Messages, modals |
image | Display an image | Messages, modals |
rich_text | Formatted text | Messages |
The Block Kit Builder (https://app.slack.com/block-kit-builder) lets you compose blocks visually and preview the JSON.
7. Socket Mode vs HTTP Mode
| Category | Socket Mode | HTTP Mode |
|---|---|---|
| Connection | WebSocket (bidirectional) | HTTP POST (one-way) |
| Public URL needed | Not needed | Needed (an HTTPS endpoint) |
| Firewall | Works from behind a firewall | Must be reachable from outside |
| Tokens | App Token (xapp-) + Bot Token | Bot Token + Signing Secret |
| Local development | Very convenient (no ngrok) | Needs a tunneling tool such as ngrok |
| Scalability | A single instance is recommended | Multiple instances can be load-balanced |
| Stability | Needs reconnection on a long-lived link | Stateless, stable |
| Recommended for | Internal tools, small teams | Large services, Marketplace apps |
| Bolt setup | SocketModeHandler | App() + WSGI/ASGI |
Socket Mode Runner Code
# Socket Mode (development, internal tools)
from slack_bolt import App
from slack_bolt.adapter.socket_mode import SocketModeHandler
app = App(token=os.environ["SLACK_BOT_TOKEN"])
handler = SocketModeHandler(app, os.environ["SLACK_APP_TOKEN"])
handler.start()
HTTP Mode Runner Code (Flask)
# HTTP Mode (production, external services)
from slack_bolt import App
from slack_bolt.adapter.flask import SlackRequestHandler
from flask import Flask, request
app = App(
token=os.environ["SLACK_BOT_TOKEN"],
signing_secret=os.environ["SLACK_SIGNING_SECRET"],
)
flask_app = Flask(__name__)
handler = SlackRequestHandler(app)
@flask_app.route("/slack/events", methods=["POST"])
def slack_events():
return handler.handle(request)
@flask_app.route("/slack/commands", methods=["POST"])
def slack_commands():
return handler.handle(request)
@flask_app.route("/slack/interactions", methods=["POST"])
def slack_interactions():
return handler.handle(request)
if __name__ == "__main__":
flask_app.run(port=3000)
How to choose: Socket Mode is convenient during development or for an internal-only bot. If you plan to deploy to many workspaces or list the app on the Marketplace, use HTTP Mode.
8. Bot Framework Comparison -- Slack Bolt vs discord.py vs Telegram Bot
| Category | Slack Bolt (Python) | discord.py | python-telegram-bot |
|---|---|---|---|
| Platform | Slack | Discord | Telegram |
| Install | pip install slack-bolt | pip install discord.py | pip install python-telegram-bot |
| Authentication | OAuth 2.0 + Bot Token | Bot Token | Bot Token (BotFather) |
| Event delivery | Socket Mode / HTTP | Gateway (WebSocket) | Polling / Webhook |
| UI framework | Block Kit (rich blocks) | Embed, Button, Select | InlineKeyboard, ReplyKeyboard |
| Modal support | Supported (views_open) | Modal (discord.ui) | Not supported (use a conversation flow) |
| Slash commands | Natively supported | Application Command | BotCommand |
| File upload | files.upload API | File object | send_document |
| Rate limits | Per tier (1-100+ req/min) | 50 req/sec (global) | 30 msg/sec (group), 1/sec (individual) |
| Async support | AsyncApp provided | Async by default | asyncio by default |
| Docs quality | Excellent (thorough official guide) | Excellent (active community) | Excellent (many examples) |
| Enterprise | Slack Enterprise Grid | Limited | Limited |
9. Deployment Strategy -- Docker and Kubernetes
9.1 Dockerfile
FROM python:3.12-slim
WORKDIR /app
# copy the dependencies first (to use layer caching)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# copy the application code
COPY . .
# run as a non-root user
RUN adduser --disabled-password --gecos "" botuser
USER botuser
# health check (Socket Mode needs a separate HTTP server for this)
HEALTHCHECK \
CMD python -c "print('healthy')" || exit 1
CMD ["python", "app.py"]
9.2 Docker Compose
# docker-compose.yml
services:
slack-bot:
build: .
env_file: .env
restart: unless-stopped
deploy:
resources:
limits:
memory: 256M
cpus: '0.5'
logging:
driver: json-file
options:
max-size: '10m'
max-file: '3'
9.3 Kubernetes Deployment
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: slack-bot
labels:
app: slack-bot
spec:
replicas: 1 # a single instance is recommended for Socket Mode
selector:
matchLabels:
app: slack-bot
template:
metadata:
labels:
app: slack-bot
spec:
containers:
- name: slack-bot
image: myregistry/slack-bot:latest
resources:
requests:
memory: '128Mi'
cpu: '100m'
limits:
memory: '256Mi'
cpu: '500m'
envFrom:
- secretRef:
name: slack-bot-secrets
livenessProbe:
exec:
command:
- python
- -c
- "print('alive')"
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
exec:
command:
- python
- -c
- "print('ready')"
initialDelaySeconds: 5
periodSeconds: 10
restartPolicy: Always
---
apiVersion: v1
kind: Secret
metadata:
name: slack-bot-secrets
type: Opaque
stringData:
SLACK_BOT_TOKEN: 'xoxb-your-bot-token'
SLACK_APP_TOKEN: 'xapp-your-app-token'
SLACK_SIGNING_SECRET: 'your-signing-secret'
A caution when deploying Socket Mode: Socket Mode holds only one WebSocket connection, so set replicas: 1. If you need high availability, switch to HTTP Mode and put a load balancer in front.
10. Error Handling and Rate Limit Management
10.1 Handling Rate Limits
The Slack API has a different rate limit tier for each method.
| Tier | Allowance | Representative method |
|---|---|---|
| Tier 1 | 1 req/min | admin.* |
| Tier 2 | 20 req/min | conversations.create |
| Tier 3 | 50 req/min | chat.postMessage |
| Tier 4 | 100+ req/min | users.info |
| Special | 1 req/sec (burst) | chat.postMessage (per workspace) |
# utils/rate_limiter.py
import time
import logging
from slack_sdk.errors import SlackApiError
from slack_sdk.http_retry.builtin_handlers import RateLimitErrorRetryHandler
logger = logging.getLogger(__name__)
def configure_rate_limit_handler(client):
"""Add the rate limit handler to the WebClient"""
rate_limit_handler = RateLimitErrorRetryHandler(max_retry_count=3)
client.retry_handlers.append(rate_limit_handler)
return client
def safe_api_call(func, *args, max_retries=3, **kwargs):
"""A safe API call wrapper that accounts for rate limits"""
for attempt in range(max_retries):
try:
return func(*args, **kwargs)
except SlackApiError as e:
if e.response.status_code == 429:
retry_after = int(e.response.headers.get("Retry-After", 5))
logger.warning(
f"Rate limited. Retrying after {retry_after}s "
f"(attempt {attempt + 1}/{max_retries})"
)
time.sleep(retry_after)
else:
logger.error(f"Slack API error: {e.response['error']}")
raise
raise Exception(f"Max retries ({max_retries}) exceeded for API call")
# usage example
# result = safe_api_call(
# client.chat_postMessage,
# channel="#general",
# text="Hello!"
# )
10.2 The Global Error Handling Pattern
# app-level error handler
@app.error
def global_error_handler(error, body, logger):
"""Catch unhandled errors across the whole app"""
logger.exception(f"Unhandled error: {error}")
# branch on the error type
if isinstance(error, SlackApiError):
error_code = error.response.get("error", "unknown_error")
logger.error(f"Slack API Error: {error_code}")
if error_code == "channel_not_found":
logger.warning("Channel not found - check channel ID or bot permissions")
elif error_code == "not_in_channel":
logger.warning("Bot is not in the channel - invite the bot first")
elif error_code == "token_revoked":
logger.critical("Bot token has been revoked!")
else:
logger.error(f"Non-Slack error: {type(error).__name__}: {error}")
11. Failure Cases and Recovery Procedures
11.1 Common Failure Cases
Case 1: a timeout caused by not calling ack()
- Symptom: an "operation_timeout" error appears when the slash command runs
- Cause:
ack()was not called at the start of the handler, or slow work was done beforeack() - Fix: always call
ack()on the first line of the handler. Handle slow work separately, afterack()
Case 2: the Socket Mode connection drops
- Symptom: the bot suddenly stops responding
- Cause: an unstable network, a server restart, or the WebSocket session expiring on Slack's side
- Fix:
SocketModeHandlersupports automatic reconnection, but add a second layer of protection with a process supervisor (systemd, supervisor) or Kubernetes' restartPolicy
Case 3: an infinite loop of bot messages
- Symptom: the bot reacts to its own messages and sends messages endlessly
- Cause: the
bot_idcheck is missing from themessageevent handler - Fix: add the guard
if event.get("bot_id"): return
Case 4: modal input validation fails
- Symptom: nothing happens when the modal is submitted
- Cause: the error was returned without
response_action="errors"on theack()call - Fix: on a validation failure, use the
ack(response_action="errors", errors={...})pattern
Case 5: an expired token or insufficient scopes
- Symptom: a
missing_scopeorinvalid_autherror - Cause: a required OAuth scope was never added, or the token was never reissued
- Fix: add the scope under OAuth & Permissions and reinstall the app
11.2 Recovery Procedure Checklist
When an incident happens, work through the recovery in the following order.
- Check the logs: read the error message with
docker logs slack-botorkubectl logs - Verify the token: call
curl -H "Authorization: Bearer xoxb-..." https://slack.com/api/auth.test - Check the event subscriptions: inspect the Event Subscriptions state on the Slack app settings page
- Check the network connection: for Socket Mode, the WebSocket connection state; for HTTP Mode, whether the endpoint is reachable
- Restart the process:
docker restart slack-botorkubectl rollout restart deployment/slack-bot - Recheck the scopes: if the error message contains
missing_scope, add that scope and reinstall the app
12. Operational Notes
Logging Recommendations
- Add logging to every event handler so debugging stays easy.
- Do not leave sensitive information (tokens, the full text of user messages) in the logs.
- Structured logging (JSON format) makes integration with log analysis tools straightforward.
Security Cautions
- Never commit the
.envfile to Git. Add it to.gitignorewithout fail. - When using HTTP Mode you must verify the request signature with the
signing_secret(the Bolt SDK handles this automatically). - Follow the principle of least privilege for Bot Token permissions.
Performance Optimization
- Handle slow work (external API calls, DB queries) on a separate thread after
ack(). - Using
AsyncAppraises throughput through asynchronous event handling. - A Block Kit message allows at most 50 blocks, so page long content.
13. Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| "dispatch_failed" error | No event handler is registered | Check the handler registration, e.g. @app.event("message") |
| Slash command does not respond | ack() not called, or over 3 seconds | Add ack() on the first line of the handler |
| "not_authed" error | The token is empty or wrong | Check the .env file and the environment variables |
| The modal does not open | trigger_id expired (3 seconds) | Open the modal immediately after ack() |
| Reaction events never arrive | The Event Subscription is not configured | Add a subscription for the reaction_added event |
| "missing_scope" error | A required OAuth scope is missing | Add the scope under OAuth & Permissions and reinstall |
| Socket Mode connection fails | The App Token was never created, or is wrong | Regenerate the App Token under Basic Information |
| The bot cannot post to a channel | The bot has not joined that channel | Invite the bot to the channel, or add the chat:write.public scope |
| HTTP 429 response | Rate limit exceeded | Wait as long as the Retry-After header says, then retry |
| Duplicate events received | Slack's redelivery mechanism | Add idempotency logic keyed on the event ID |
14. Production Deployment Checklist
Check every item below before deploying.
- Confirm the Bot Token (
xoxb-) and the App Token (xapp-) are configured correctly - Confirm every required OAuth scope has been added
- Confirm every required event is subscribed under Event Subscriptions
- Confirm Interactivity is enabled
- Confirm the slash commands are registered in the Slack app settings
- Confirm the
.envfile is listed in.gitignore - Confirm
ack()is called in every handler - Confirm there is guard logic preventing an infinite loop on the bot's own messages
- Confirm rate limit handling is implemented
- Confirm the error handler (
@app.error) is registered - Confirm logging is configured properly
- Confirm the Docker image runs as a non-root user
- Confirm the tokens are managed through a Kubernetes Secret
- Confirm the process restart policy is set
- Confirm replicas is 1 when using Socket Mode
Conclusion
A Slack Bot is a tool that dramatically raises a team's efficiency. With the Python Bolt SDK you can implement every Slack interaction - event handling, slash commands, modals, Block Kit - in concise code.
The key points:
- ack() is the starting point of every interaction handler. Always remember the 3-second timeout.
- Socket Mode is best for development and internal tools; HTTP Mode suits production-scale services.
- Compose rich messages with Block Kit, and take structured user input with modals.
- You must implement rate limit handling and an error recovery procedure.
- Build a stable production deployment environment with Docker and Kubernetes.