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
Answer first
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.
Workload and what breaks
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.
Where shortcuts fail
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.
How Inquir helps
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.
What you get
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.
What to do next
Slack slash command flow
Verify HMAC, parse command
Check x-slack-signature with HMAC-SHA256. Extract command, text, user_id, response_url from URL-encoded body.
Trigger async orchestration, return 200
Call global.durable.startNew with command params including response_url. Return empty 200 response immediately.
Post result via response_url
Pipeline step performs the real work, then POSTs a Block Kit response to response_url.
Code example
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.
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: '' }; }
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 }; }
When it fits
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
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.