Vercel Logo

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

  1. Understand the ack-first pattern: await ack() before any slow work
  2. Add logging and a 2-second delay to sample-command.ts to demonstrate the pattern
  3. Test /sample-command and 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:

  1. Add logging and a 2-second delay to server/listeners/commands/sample-command.ts AFTER ack()
  2. Add correlation logging (from lesson 2.2) to track the command execution
  3. Update the response message to explain the ack-first pattern
  4. Test /sample-command and 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.correlation in your logs (continuing the pattern from lesson 2.2)
  • Add context to the destructured parameters to access correlation fields
Commands Use respond(), Not say()

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

  1. Test the ack-first pattern:

    • Run slack run
    • Use /sample-command in Slack
    • Notice instant acknowledgment (command accepted immediately)
    • After 2 seconds, you get the final response
  2. Check logs:

    [INFO]  bolt-app { event_id: '...', ts: '...', ... } Starting slow operation...
    [INFO]  bolt-app { event_id: '...', ts: '...', ... } Slow operation complete
    

    Note: You won't see Bolt's internal [@vercel/slack-bolt] ack() call begins debug logs unless logLevel is set to LogLevel.DEBUG in server/app.ts. The correlation fields from lesson 2.2 will appear if you added them.

  3. (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 2000 to 4000 (exceeds 3-second limit)
    • Run /sample-command again
    • In Slack: User sees "operation_timeout" error (looks broken to anyone watching)
    • In logs: VercelReceiverError: Request timeout after 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 context parameter 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-command and 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:

/slack-agent/server/listeners/commands/sample-command.ts
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(), use respond() for both success and error messages (commands use respond(), events use say())
  • You can only call ack() once per request. If you try to call it twice, Bolt throws an error. Once acknowledged, use respond() 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() before views.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 first await in 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—use respond() or say() to send error details
Long-Running Operations (>30 seconds)

For work that takes longer than Slack's patience (AI inference, batch processing, report generation), ack()respond() isn't enough. You'll need:

  1. Acknowledge immediately: await ack() with message "This will take a few minutes..."
  2. Queue the work: Use background jobs (not covered in this course, but common pattern)
  3. Post result later: Use chat.postMessage() with the original channel/thread_ts when 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.