Inquir Compute logoInquir Compute
Use case · Slack

Process Slack webhooks and slash commands serverlessly

Slack requires a 3-second response to slash commands or marks your app as slow. Build a serverless handler that verifies HMAC, returns immediately, and posts the real response via response_url from an async pipeline—no tight response window pressure.

Last updated: 2026-04-20

Direct answer

Process Slack webhooks and slash commands serverlessly. The Slack handler verifies the HMAC signature, extracts slash command parameters, triggers an async pipeline, and returns HTTP 200 with an empty body within milliseconds.

When it fits

  • Slash commands that trigger LLM inference, database lookups, or external API calls
  • Event subscriptions that feed data into pipelines or trigger notifications

Tradeoffs

  • Any external API call inside the synchronous Slack handler risks exceeding the 3-second window. LLM inference, database queries with joins, and third-party API calls all regularly take 2–10 seconds.
  • Without an async handoff, users see "This app took too long to respond" and the command appears to fail even if it eventually succeeds.

The Slack 3-second challenge

  • Slash commands: Slack requires HTTP 200 within 3 seconds or marks app as unresponsive
  • Event subscriptions: acknowledgement required within 3 seconds, processing can continue
  • Interactive components (buttons, modals): same 3-second window

Most Slack operations—LLM calls, database queries, external API calls—take longer than 3 seconds. The standard pattern is to acknowledge immediately, do the work asynchronously, and post the result back via response_url or the Slack Web API.

Inline Slack processing is almost always too slow

Any external API call inside the synchronous Slack handler risks exceeding the 3-second window. LLM inference, database queries with joins, and third-party API calls all regularly take 2–10 seconds.

Without an async handoff, users see "This app took too long to respond" and the command appears to fail even if it eventually succeeds.

Verify fast, respond immediately, work async

The Slack handler verifies the HMAC signature, extracts slash command parameters, triggers an async pipeline, and returns HTTP 200 with an empty body within milliseconds.

The pipeline step does the real work—calls the LLM, queries the database, fetches external data—and posts the result to Slack via response_url or chat.postMessage.

Slack webhook handling features

HMAC-SHA256 request verification

Slack signs requests with HMAC-SHA256 over timestamp + raw body. Verify before processing any command.

3-second fast ACK

Return 200 immediately after verification. Never await slow work before responding.

response_url async response

Post the real result to response_url from the async pipeline step. Supports ephemeral and in_channel responses.

Slash command routing

Route different slash commands to different pipeline jobs based on the command field.

Slack slash command flow

1

Verify HMAC, parse command

Check x-slack-signature with HMAC-SHA256. Extract command, text, user_id, response_url from URL-encoded body.

2

Trigger async orchestration, return 200

Call global.durable.startNew with command params including response_url. Return empty 200 response immediately.

3

Post result via response_url

Pipeline step performs the real work, then POSTs a Block Kit response to response_url.

Slack slash command handler

Verify, ACK, trigger async pipeline—all within the 3-second window. The pipeline step does the work and posts back via response_url.

webhooks/slack.mjs (fast ACK handler)
import { createHmac, timingSafeEqual } from 'node:crypto';

export async function handler(event) {
  const body = event.body ?? '';
  const timestamp = event.headers['x-slack-request-timestamp'] ?? '';
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
    return { statusCode: 400, body: 'stale request' };
  }
  const sigBase = `v0:${timestamp}:${body}`;
  const expected = 'v0=' + createHmac('sha256', process.env.SLACK_SIGNING_SECRET).update(sigBase).digest('hex');
  const received = event.headers['x-slack-signature'] ?? '';
  if (!timingSafeEqual(Buffer.from(expected), Buffer.from(received))) {
    return { statusCode: 401, body: 'invalid signature' };
  }
  const params = Object.fromEntries(new URLSearchParams(body));
  await global.durable.startNew('slack-command', undefined, {
    command: params.command,
    text: params.text,
    userId: params.user_id,
    channelId: params.channel_id,
    responseUrl: params.response_url,
  });
  return { statusCode: 200, body: '' };
}
jobs/slack-command.mjs (async work + response)
export async function handler(event) {
  const { command, text, userId, responseUrl } = event.payload ?? {};
  const result = await processCommand(command, text, userId);
  await fetch(responseUrl, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      response_type: 'in_channel',
      blocks: [{ type: 'section', text: { type: 'mrkdwn', text: result } }],
    }),
  });
  return { command, userId, result };
}

Use this for Slack automation

When this works

  • Slash commands that trigger LLM inference, database lookups, or external API calls
  • Event subscriptions that feed data into pipelines or trigger notifications

When to skip it

  • Simple Slack incoming webhooks that just POST messages—those do not require HMAC verification

FAQ

What about Slack event subscriptions?

Respond to the challenge value within 3 seconds during URL verification. For production events, acknowledge and delegate to async pipelines exactly as slash commands.

How do I handle Slack modal submissions?

Modal submissions (view_submission) work the same way: verify HMAC, return 200 immediately, process async via pipeline.

Inquir Compute logoInquir Compute

The simplest way to run AI agents and backend jobs without infrastructure.

Contact info@inquir.org

© 2025 Inquir Compute. All rights reserved.