Acknowledgment Patterns: Ack First, Work Second
Slack enforces a hard 3-second timeout on commands, shortcuts, and actions. Don't acknowledge in time, users see an error. Your bot needs to query databases, call APIs, run AI inference—all >3 seconds. Solution: call ack() immediately (target under 1 second, not 3—network latency eats the buffer), then do heavy work. Slack's happy, users see instant feedback, work completes in the background.
Outcome
Understand Slack's acknowledgment requirements and implement the ack-first pattern so users never see timeout errors.
Fast Track
- Understand the ack-first pattern:
await ack()before any slow work - Add logging and a 2-second delay to
sample-command.tsto demonstrate the pattern - Test
/sample-commandand verify instant ack despite delay
Building on Previous Lessons
- From Repository Flyover: Stateless handlers must acknowledge immediately
- From Bolt Middleware: Correlation IDs track operations even after ack() returns
- Sets up Section 3: All interaction surfaces follow this ack-first pattern
The Acknowledgment Flow
User clicks button → [Your handler] → ack() → Heavy processing → Final response
↑ Must happen within 3 seconds
ack() tells Slack "I received your request" and prevents timeout errors. It's separate from your actual response.
Hands-On Exercise 2.3
Enhance the sample command to demonstrate the ack-first pattern:
Requirements:
- Add logging and a 2-second delay to
server/listeners/commands/sample-command.tsAFTERack() - Add correlation logging (from lesson 2.2) to track the command execution
- Update the response message to explain the ack-first pattern
- Test
/sample-commandand verify instant acknowledgment despite the delay
Implementation hints:
- The template already calls
ack()first—you're adding delay/logging to demonstrate it - Use
await new Promise(resolve => setTimeout(resolve, 2000))to simulate slow work (in real code this would be an API call, database query, or AI inference) - Include
context.correlationin your logs (continuing the pattern from lesson 2.2) - Add
contextto the destructured parameters to access correlation fields
Slash commands must use respond() for follow-up messages after ack(). The respond() function sends messages back through the command's response URL (ephemeral by default) and works even if your bot isn’t a member of the channel yet. say() posts into a channel using event.channel and is meant for events like mentions and DMs. Mixing them up leads to confusing errors (channel_not_found, messages not appearing where you expect) and breaks the ack→respond lifecycle this lesson is teaching.
Try It
-
Test the ack-first pattern:
- Run
slack run - Use
/sample-commandin Slack - Notice instant acknowledgment (command accepted immediately)
- After 2 seconds, you get the final response
- Run
-
Check logs:
[INFO] bolt-app { event_id: '...', ts: '...', ... } Starting slow operation... [INFO] bolt-app { event_id: '...', ts: '...', ... } Slow operation completeNote: You won't see Bolt's internal
[@vercel/slack-bolt] ack() call beginsdebug logs unlesslogLevelis set toLogLevel.DEBUGinserver/app.ts. The correlation fields from lesson 2.2 will appear if you added them. -
(Optional) See what a timeout looks like:
- Do this in a test channel where breaking the bot publicly is okay
- Temporarily move lines 18-20 (the delay + logging) to BEFORE line 14 (
await ack()) - Change
2000to4000(exceeds 3-second limit) - Run
/sample-commandagain - In Slack: User sees "operation_timeout" error (looks broken to anyone watching)
- In logs:
VercelReceiverError: Request timeoutafter 3 seconds - Revert: Move delay back after
ack()and change back to 2000 before committing
Commit
git add -A
git commit -m "feat(ack): demonstrate acknowledgment timing patterns
- Add slow operation simulation to sample command
- Test timeout behavior when ack is delayed
- Fix by moving ack before heavy processing
- Verify users never see timeout errors"Done-When
- Added
contextparameter to sample command handler - Added correlation logging with
...context.correlation(continuing lesson 2.2 pattern) - Added 2-second delay after
ack()to demonstrate the pattern - Updated all log calls to use structured logging with correlation fields
- Tested
/sample-commandand verified instant acknowledgment despite delay - Understand the ack-first pattern applies to all interactions (commands, shortcuts, actions, views)
Solution
Update server/listeners/commands/sample-command.ts to demonstrate the ack-first pattern:
import type {
AllMiddlewareArgs,
SlackCommandMiddlewareArgs,
} from "@slack/bolt";
export const sampleCommandCallback = async ({
ack,
respond,
logger,
context,
}: AllMiddlewareArgs & SlackCommandMiddlewareArgs) => {
try {
// ✅ Acknowledge immediately (always first)
await ack();
// Heavy work happens after acknowledgment
// Simulate slow API call or database query with a 2-second delay
logger.info({ ...context.correlation }, "Starting slow operation...");
await new Promise(resolve => setTimeout(resolve, 2000));
logger.info({ ...context.correlation }, "Slow operation complete");
await respond({
text: "Command completed! This took 2 seconds but you didn't see a timeout because we ack'd first.",
response_type: "ephemeral",
});
} catch (error) {
logger.error({
...context.correlation,
error: error instanceof Error ? error.message : String(error),
}, "Slash command handler failed");
// After ack(), use respond() for error messages (can't ack() twice)
try {
await respond({
text: "Sorry, something went wrong processing your command.",
response_type: "ephemeral",
});
} catch (respondError) {
logger.error({
...context.correlation,
error: respondError instanceof Error ? respondError.message : String(respondError),
}, "Failed to send error response");
}
}
};Key points:
ack()is always first—before any database calls, API requests, or AI inference- After
ack(), userespond()for both success and error messages (commands userespond(), events usesay()) - You can only call
ack()once per request. If you try to call it twice, Bolt throws an error. Once acknowledged, userespond()for all follow-up messages.
What's Next
You'll use this same ack-first pattern throughout Section 3 (Interaction Surfaces):
- Lesson 3.1: Slash commands (you just learned this)
- Lesson 3.2: Shortcuts opening modals (
ack()beforeviews.open()) - Lesson 3.3: Button actions and modal submissions
The rule is always the same: acknowledge instantly, then do the work.
Troubleshooting
Still seeing timeouts:
- Verify
ack()is the firstawaitin your handler - Check that no database queries or API calls happen before
ack() - Aim for
ack()within 1 second, not 3 (network latency buffer)
Error after ack():
- You can only call
ack()once per request (Bolt throws if you try twice) - Commands use
respond()for follow-up messages (ephemeral by default) - Events use
say()for follow-up messages (visible to all) - If an error happens after
ack(), you can't un-acknowledge—userespond()orsay()to send error details
For work that takes longer than Slack's patience (AI inference, batch processing, report generation), ack() → respond() isn't enough. You'll need:
- Acknowledge immediately:
await ack()with message "This will take a few minutes..." - Queue the work: Use background jobs (not covered in this course, but common pattern)
- Post result later: Use
chat.postMessage()with the originalchannel/thread_tswhen work completes
Alternative: For AI agents, you can stream responses in real-time instead of queuing (you'll see this in Section 5).
What's Next
You've learned the core Bolt patterns: manifests control features/scopes/events, middleware adds cross-cutting concerns, and acknowledgment prevents timeouts. Section 3 builds on these foundations with interaction surfaces—slash commands, shortcuts, modals, views, and App Home. Every interaction follows the same ack-first pattern you just learned.
Was this helpful?