Uptime Kuma Discord Webhook: Grafana and Alertmanager Alerts
Send server monitoring alerts to Discord: Uptime Kuma discord webhook setup, Grafana contact point, Alertmanager discord_configs and a cron health check.
On this page
- Why monitoring alerts belong in Discord
- Uptime Kuma Discord webhook
- Grafana Discord webhook (contact point)
- Alertmanager Discord receiver
- A cron health check in bash
- Colour-coded embeds for up and down
- Avoiding alert storms
- Common errors
- FAQ
- Does Uptime Kuma need a Discord bot to send alerts?
- Which Alertmanager version supports discord_configs?
- Can I send Grafana or Alertmanager alerts into a thread?
- How do I stop getting a message every minute while a service is down?
- Next steps
Why monitoring alerts belong in Discord
An outage that lands in #alerts is seen in seconds; the same outage in an inbox is seen after lunch. Webhooks make this cheap: no bot, no OAuth, no server-side code. Create one in Server Settings → Integrations → Webhooks, copy the URL, and every tool below can POST to it.
Four setups, in order of effort:
- Uptime Kuma — a native Discord notification type; paste the URL and you are done.
- Grafana Alerting — a contact point of type Discord.
- Prometheus Alertmanager — the
discord_configsreceiver, available since Alertmanager 0.25. - A cron job — a short bash script for the box that runs none of the above.
Then colour-coding, so a red card means “down” at a glance, and storm control, so one bad night does not turn into four hundred pings.
Use a dedicated webhook for the alert channel (how to get the URL) and treat it like a password: anyone holding the URL can post, and the only fix is deleting and recreating it.
Uptime Kuma Discord webhook
Uptime Kuma has Discord as a first-class notification type; no generic webhook mapping needed.
- Open Settings → Notifications → Setup Notification.
- Set Notification Type to Discord.
- Paste the URL into Discord Webhook URL.
- Optionally set Bot Display Name (this becomes the
usernameoverride, so it cannot contain “discord” or “clyde”) and a Prefix Custom Message. - Click Test, then Save. Tick Apply on all existing monitors, or attach it per monitor on the monitor’s edit page.
The card itself is fixed: on DOWN, a red embed titled ”❌ Your service name went down. ❌” with Service Name, Service URL, Time and Error fields; on UP, a green embed with a Ping field instead of the error. You write no JSON; the display name and the prefix are the only styling knobs.
The prefix is where a role ping goes. Uptime Kuma sends it as the message content, so <@&ROLE_ID> there mentions the on-call role above the embed. If it renders as plain text, the id is wrong (copy it with Developer Mode → right-click the role → Copy Role ID); how allowed_mentions decides who gets pinged is in mentions in webhooks.
Storm control lives on each monitor: Retries (consecutive failed checks before the monitor flips to DOWN), Heartbeat Retry Interval, and Resend Notification if Down X times consecutively (leave it at 0 for exactly one DOWN message).
Grafana Discord webhook (contact point)
Grafana Alerting ships a Discord integration:
- Alerting → Contact points → Create contact point.
- Integration: Discord. Paste the webhook URL.
- Keep Title and Message content on their defaults (
{{ template "default.title" . }}and{{ template "default.message" . }}). - Test, then Save.
- Alerting → Notification policies: point the default policy, or a child policy with label matchers, at this contact point. Otherwise the contact point exists but nothing is routed to it.
Grafana sends the message as the text above an embed; the embed is red while the group is firing and green on resolve, and carries the title and a link back to Grafana. Title and message are trimmed to Discord’s limits (256 and 2000 characters), so a rule with thirty labels will not produce a 400.
As provisioning YAML:
# provisioning/alerting/discord.yaml
apiVersion: 1
contactPoints:
- orgId: 1
name: discord-ops
receivers:
- uid: discord-ops
type: discord
settings:
url: https://discord.com/api/webhooks/YOUR_ID/YOUR_TOKEN
title: '{{ template "default.title" . }}'
message: '{{ template "default.message" . }}'
use_discord_username: false
policies:
- orgId: 1
receiver: discord-ops
group_by: ['alertname', 'instance']
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
The three timing fields are your storm control: group_wait bundles alerts arriving in the first 30 seconds into one message, group_interval caps how often that group sends updates, and repeat_interval sets how often a still-firing alert is re-sent. With a four-hour repeat, a weekend outage produces a handful of messages, not hundreds.
Alertmanager Discord receiver
Since Alertmanager 0.25 the Discord integration is built in; older releases needed a bridge or a generic webhook receiver behind a payload-reshaping proxy, and upgrading is the shorter path.
# alertmanager.yml
global:
resolve_timeout: 5m
route:
receiver: discord
group_by: ['alertname', 'job']
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
routes:
- matchers:
- severity = critical
receiver: discord-critical
repeat_interval: 1h
receivers:
- name: discord
discord_configs:
- webhook_url: https://discord.com/api/webhooks/YOUR_ID/YOUR_TOKEN
send_resolved: true
- name: discord-critical
discord_configs:
- webhook_url: https://discord.com/api/webhooks/OTHER_ID/OTHER_TOKEN
send_resolved: true
title: '{{ template "discord.default.title" . }}'
message: '{{ template "discord.default.message" . }}'
inhibit_rules:
- source_matchers:
- severity = critical
target_matchers:
- severity = warning
equal: ['alertname', 'instance']
Alertmanager sends one embed per notification, red for firing and green for resolved, description rendered from message and cut to the embed limit. Validate and reload:
amtool check-config alertmanager.yml
curl -X POST http://localhost:9093/-/reload
Keep webhook_url out of git (newer releases accept webhook_url_file; check the docs for your version), and remember that for: in the Prometheus rule is the first line of defence: with for: 5m nothing reaches Alertmanager until the condition has held for five minutes.
A cron health check in bash
For a VPS with no monitoring stack, a script that runs every minute and posts only on state change:
#!/usr/bin/env bash
# /usr/local/bin/healthcheck.sh
set -u
WEBHOOK_URL="${DISCORD_WEBHOOK_URL:?set DISCORD_WEBHOOK_URL}"
TARGET="https://example.com/health"
STATE_FILE="/var/tmp/healthcheck.state"
FAIL_THRESHOLD=3
# curl prints 000 when the connection itself fails
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 10 "$TARGET")
prev=$(cat "$STATE_FILE" 2>/dev/null || echo "up:0")
prev_status=${prev%%:*}
fails=${prev##*:}
if [[ "$code" == "200" ]]; then
status=up; fails=0
else
status=down; fails=$((fails + 1))
# not enough consecutive failures yet: keep the old state, stay quiet
[[ $fails -lt $FAIL_THRESHOLD ]] && status=$prev_status
fi
echo "$status:$fails" > "$STATE_FILE"
[[ "$status" == "$prev_status" ]] && exit 0
if [[ "$status" == "down" ]]; then
color=15548997; title="🔴 $TARGET is DOWN"
else
color=5763719; title="🟢 $TARGET is back UP"
fi
now=$(date -u +%Y-%m-%dT%H:%M:%SZ)
curl -s -o /dev/null -w 'discord: %{http_code}\n' --max-time 10 \
-H "Content-Type: application/json" \
-d @- "$WEBHOOK_URL" <<EOF
{
"embeds": [{
"title": "$title",
"color": $color,
"fields": [
{"name": "HTTP status", "value": "$code", "inline": true},
{"name": "Checked from", "value": "$(hostname)", "inline": true}
],
"timestamp": "$now"
}]
}
EOF
The state file holds up:0 or down:N. Three consecutive non-200 answers flip it to down and send one red card; the first 200 afterwards sends one green card; everything in between is silent. Install with chmod +x and a crontab entry:
DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/YOUR_ID/YOUR_TOKEN
* * * * * /usr/local/bin/healthcheck.sh >> /var/log/healthcheck.log 2>&1
To have the recovery update the red card instead of adding a green one, send the DOWN message with ?wait=true, keep the returned id in the state file and PATCH /messages/{id} when the service is back (editing and deleting webhook messages).
Colour-coded embeds for up and down
Keep the palette tiny and never change what each colour means:
| State | Colour | color value |
|---|---|---|
| Down, firing | red #ED4245 | 15548997 |
| Up, resolved | green #57F287 | 5763719 |
| Degraded, warning | yellow #FEE75C | 16705372 |
| Maintenance, info | blurple #5865F2 | 5793266 |
Rules for every tool:
coloris a decimal integer. A hex string such as"#ED4245"is rejected with a 400.- Put the state in the title too: push notifications show text only, and not everyone distinguishes red from green.
- Send
timestampin ISO 8601 UTC; Discord renders it in each reader’s timezone. - Stay inside the embed limits when you build the payload yourself (256 characters for the title, 4096 for the description, 25 fields); Grafana and Alertmanager trim for you, your bash script does not.
- More decimal values are in the embed colours guide.
Avoiding alert storms
A channel that fires constantly gets muted, and a muted channel is worse than none.
- Never alert on one failed check. Retries in Uptime Kuma,
for:in Prometheus rules, a pending period in Grafana,FAIL_THRESHOLDin the script. - Group. One message listing twelve hosts beats twelve messages; that is what
group_byandgroup_waitare for. - Repeat in hours, not minutes.
repeat_interval: 4hfor warnings,1hfor critical. - Inhibit. The rule above silences warnings while a critical alert with the same labels is firing, so a dead host does not also report twenty dead services.
- Send resolved messages in the same style. A green card under every red one lets the channel read as pairs.
- Split severities across two webhooks. Critical goes to a channel nobody mutes; informational to one they can.
- Respect the rate limit. Fifty monitors flipping at once means fifty POSTs in one second, and Discord answers some with 429. Whether a tool retries depends on the tool; your own scripts must honour
retry_after(webhook rate limits).
Common errors
{"message":"Unknown Webhook","code":10015} on every send. The URL is truncated (tokens are long and easy to cut when pasting) or the webhook was deleted. Recreate it and update the tool.
The test button works, real alerts never arrive. The notification is not attached to the monitor (Uptime Kuma), no policy routes to the contact point (Grafana), the receiver is not referenced in route (Alertmanager), or cron did not get the environment variable (the script). The test path skips all four.
400 Bad Request from a hand-written payload. An unescaped quote or newline in a variable, a hex colour string, or a field over its limit. Validate with jq . or build the payload with jq -n --arg; the full list is in webhook errors.
Alertmanager refuses to start with an unknown field discord_configs. The binary is older than 0.25. Upgrade, or fall back to webhook_configs and a proxy.
429 Too Many Requests during an incident. Too many monitors fired in one window. Group them, raise the retry threshold, honour retry_after.
The role mention shows as text, not a ping. Wrong role id, or a role from another server. Copy the id with Developer Mode (right-click the role → Copy Role ID).
FAQ
Does Uptime Kuma need a Discord bot to send alerts?
No. The Discord notification type takes only the webhook URL; a bot is needed only for things a webhook cannot do, such as interactive acknowledge buttons.
Which Alertmanager version supports discord_configs?
Alertmanager 0.25.0 and later. Older versions reject the field at config load; upgrade or route through a generic webhook receiver and a small proxy.
Can I send Grafana or Alertmanager alerts into a thread?
Yes. Both tools POST to exactly the URL you give them, so append ?thread_id=<existing thread id> to the webhook URL. Forum channels need thread_name in the body, which these tools do not set.
How do I stop getting a message every minute while a service is down?
Alert on state changes, not on every check. Uptime Kuma sends one DOWN and one UP by default; in Grafana and Alertmanager set repeat_interval to hours; in a cron script keep the last state in a file and post only when it differs.
Next steps
Start with the tool you already run, keep the palette to red, green and yellow, and configure grouping before the first real incident, not after. For a card that looks better than the defaults, build the JSON in the Discord Webhook Builder and paste it into your script.
Related reading:
- Discord webhook notifications for automation — patterns for deploys, backups and CI.
- Discord webhook errors explained — what each 4xx response means and how to fix it.
- Discord webhook rate limits — how 429 works and how to back off correctly.