---
title: "Ack & Latency"
description: "Slack requires `ack()` within 3 seconds. Target under 2 seconds. Move heavy work after `ack()`. Read `server/listeners/commands/sample-command.ts`, `server/listeners/shortcuts/sample-shortcut.ts`, `server/listeners/actions/sample-action.ts`, and `server/listeners/views/sample-view.ts`."
canonical_url: "https://vercel.com/academy/slack-agents/acknowledgment-and-latency"
md_url: "https://vercel.com/academy/slack-agents/acknowledgment-and-latency.md"
docset_id: "vercel-academy"
doc_version: "1.0"
last_updated: "2026-04-09T11:08:43.665Z"
content_type: "lesson"
course: "slack-agents"
course_title: "Slack Agents on Vercel with the AI SDK"
prerequisites:  []
---

<agent-instructions>
Vercel Academy — structured learning, not reference docs.
Lessons are sequenced.
Adapt commands to the human's actual environment (OS, package manager, shell, editor) — detect from project context or ask, don't assume.
The lesson shows one path; if the human's project diverges, adapt concepts to their setup.
Preserve the learning goal over literal steps.
Quizzes are pedagogical — engage, don't spoil.
Quiz answers are included for your reference.
</agent-instructions>

# Ack & Latency

# 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](./repository-flyover)**: Stateless handlers must acknowledge immediately
- **From [Bolt Middleware](./bolt-nitro-middleware-and-logging)**: 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

\*\*Note: 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

```bash
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

- [x] Added `context` parameter to sample command handler
- [x] Added correlation logging with `...context.correlation` (continuing lesson 2.2 pattern)
- [x] Added 2-second delay after `ack()` to demonstrate the pattern
- [x] Updated all log calls to use structured logging with correlation fields
- [x] Tested `/sample-command` and verified instant acknowledgment despite delay
- [x] 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:

```typescript title="/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

\*\*Warning: 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.


---

[Full course index](/academy/llms.txt) · [Sitemap](/academy/sitemap.md)
