Send Useful Alerts to Slack
05 Aug 2026An alerts channel that fires on every deploy, every warning, and every retry gets muted within a week, and then it’s not an alerts channel — it’s a channel nobody reads until something’s actually on fire and three days too late. Build for signal, not for logging everything you technically could.
The basic webhook
An incoming webhook is the fastest path — no bot, no OAuth flow, just a URL you POST JSON to:
await fetch(process.env.SLACK_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: `🔴 Payment webhook failed for order ${orderId}` }),
})
That’s enough for a first version. It gets unreadable fast once you have more than a couple of alert types, because a wall of plain-text messages in a channel gives you nothing to scan.
Format with Block Kit
Block Kit gives you structured messages — fields, color-coded sections, buttons — instead of a text blob. This is what makes an alerts channel scannable at 2am instead of something you have to read line by line to parse:
await fetch(process.env.SLACK_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
blocks: [
{
type: 'section',
text: { type: 'mrkdwn', text: `*Payment webhook failed*\nOrder \`${orderId}\`` },
},
{
type: 'context',
elements: [{ type: 'mrkdwn', text: `Stripe error: ${errorCode} • <${dashboardUrl}|View in dashboard>` }],
},
],
}),
})
A context block for metadata (error codes, links, timestamps) keeps the main message short and puts the supporting detail where it’s available but not shouting for attention.
Thread follow-ups, don’t spam new messages
When the same issue fires repeatedly — a flaky third-party API retrying every minute — don’t post a new top-level message each time. Reply in a thread on the original alert, so the channel shows one line per distinct incident instead of fifty lines for one ongoing problem:
const { ts, channel } = await postAlert(initialMessage)
await threadStore.set(alertKey, { ts, channel })
// on repeat:
const thread = await threadStore.get(alertKey)
if (thread) {
await fetch('https://slack.com/api/chat.postMessage', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.SLACK_BOT_TOKEN}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ channel: thread.channel, thread_ts: thread.ts, text: 'Still failing, 4th retry.' }),
})
}
This needs the bot token API (chat.postMessage), not the incoming webhook, since replying in a thread requires referencing the original message’s timestamp — a plain webhook can’t do that.
Avoid alert fatigue on purpose
- Dedupe. If the same error fires 200 times in a minute because a queue is retrying, alert once and count the rest — “Payment webhook failed (47 times in the last 5 min)” — not 200 separate messages.
- Separate channels by severity. A
#alerts-criticalchannel that only ever contains things needing immediate action, and a#alerts-infochannel (or none at all) for things worth knowing but not worth a notification ping. Mixing them trains people to ignore both. - Rate limit per alert type. Cap a given alert type to firing once per few minutes even if the underlying condition is still true — a database connection pool exhausted for 20 minutes doesn’t need 20 separate pings, it needs one alert and one ongoing thread.
The test that matters
Ask: if this fires at 3am, does the on-call person need to wake up for it? If not, it doesn’t belong in the channel that pages people — route it to a lower-priority channel or a daily digest instead. The value of a Slack alerts channel is entirely a function of how much you can trust that everything in it is worth acting on. One noisy alert type erodes that trust for every other alert sharing the channel.