Guide to ClickHouse Webhooks: Features and Best Practices
ClickHouse webhooks, in the sense Hookdeck supports, come from ClickStack alerts. ClickStack is ClickHouse's observability stack for logs, metrics and traces, with HyperDX as its interface, and its alerts can call a webhook when a saved search or dashboard chart crosses a threshold. A Generic webhook lets you send those alerts to your own incident tooling, on-call workflow or automation, not just to Slack or PagerDuty.
This guide covers how ClickStack alerts fire, how to set up a Generic webhook with a secret header, how to authenticate it, and the gaps in what ClickHouse documents.
What are ClickHouse webhooks?
ClickStack has two kinds of alert. Search alerts fire on the count of results from a saved search, and dashboard chart alerts fire on a chart's value, including charts written in SQL. Each alert is evaluated on a recurring interval, and when its condition is met it sends a notification to the webhook you picked.
A webhook in ClickStack has a service type: Slack, PagerDuty or Generic. This guide, and Hookdeck's ClickHouse source, cover the Generic type only. The ClickHouse Cloud control plane has no event webhooks of its own.
Generic webhooks are not signed. You authenticate them by adding a secret header under Webhook headers when you create the webhook, and your receiver checks it.
ClickHouse webhook features
| Feature | Details |
|---|---|
| Sender | ClickStack alerts (search alerts and dashboard chart alerts) |
| Service type covered | Generic |
| Configuration | Alert dialog > Add New Webhook |
| Fields | Name, optional description, Webhook URL, optional headers, optional body |
| Body template variables | {{title}}, {{body}}, {{link}} |
| When it sends | Each evaluation where the alert condition is met |
| Recovery notification | None |
| Authentication | A header you add under Webhook headers |
| Signature and timestamp | None |
| Method, content type | Not documented |
| Retries and timeout | Not documented |
The payload
There is no fixed payload. The Webhook body is an optional template you write, and ClickStack currently supports three template variables in it: {{title}}, {{body}} and {{link}}. ClickHouse does not document their exact contents beyond the names, so look at a real alert before relying on them. A simple JSON template looks like this:
{
"source": "clickstack",
"title": "{{title}}",
"body": "{{body}}",
"link": "{{link}}"
}
ClickStack adds no event type or alert ID of its own. Anything you want to route or deduplicate on has to come from these three variables or from fixed values you put in the template, such as an alert name or severity.
ClickHouse's docs do not say what the body is when you leave the template empty, which HTTP method is used, or which Content-Type is sent. Capture a test delivery before you write the parser.
See what ClickStack sends. Use a Hookdeck Inspect URL as the Webhook URL and let an alert fire to see the method, headers and body, with no account or setup required.
How alerts fire
The way ClickStack evaluates alerts decides how many requests your endpoint receives.
- Each alert is evaluated on its interval. For SQL chart alerts the interval is one of 1m, 5m, 15m, 30m, 1h, 6h, 12h or 1d, and it is also the time window each evaluation looks at.
- Each evaluation is independent. If the condition is met, the alert fires and a notification is sent. If it is not met, nothing is sent.
- So a condition that stays true sends a notification every interval, and there is no "resolved" notification when it clears. The alert history shows this as a run of red bars followed by green ones.
- A search alert with a grouped by value, or a SQL alert with grouping columns, is tracked per group, and ClickStack sends an alert for each group that breaches the threshold.
- If intervals are skipped because of errors or slow queries, the next run covers the missed time too, so one notification can describe a longer window than usual.
Setting up ClickHouse webhooks
Webhooks are created while you create or edit an alert.
- Generate a long random secret, for example with
openssl rand -hex 32. - Open a saved search and click Alerts, or open a chart on a saved dashboard and select Add Alert.
- Set the condition, threshold and interval.
- Where you choose the webhook, select Add New Webhook.
- Give the webhook a name (required) and, optionally, a description.
- Choose the Generic service type and enter the Webhook URL.
- Under Webhook headers, add a secret header, for example
Authorization: Bearer <your secret>. - Optionally, write a Webhook body template using
{{title}},{{body}}and{{link}}. - Save the alert. For a dashboard chart alert, save the dashboard to activate it.
Once created, the webhook can be reused by other alerts, so one secret header covers every alert that uses it. Slack API and PagerDuty service types are available only in ClickHouse Cloud, but Generic webhooks are available in ClickStack Open Source too.
To route through Hookdeck, create a Hookdeck source with the ClickHouse type and use its URL as the Webhook URL. The source covers ClickStack alert webhooks of type Generic only, and has two fields:
- API Key Header: the header you added under Webhook headers, such as
Authorization. - API Key: the exact header value, including any
Bearerprefix.
The source accepts POST. ClickHouse does not document the method, so check your first delivery in Hookdeck to confirm it arrived.
Securing ClickHouse webhooks
Look up the header you configured and compare its whole value with your secret in constant time. Since the body format is not documented, keep it as text if it is not JSON.
const crypto = require('crypto');
const express = require('express');
const HEADER = (process.env.CLICKHOUSE_WEBHOOK_HEADER || 'authorization').toLowerCase();
const EXPECTED = process.env.CLICKHOUSE_WEBHOOK_SECRET; // the whole value, e.g. "Bearer 3f9a..."
function safeEqual(received, expected) {
const a = Buffer.from(received, 'utf8');
const b = Buffer.from(expected, 'utf8');
// timingSafeEqual throws on unequal lengths, so check first.
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
const app = express();
app.post('/webhooks/clickhouse', express.raw({ type: '*/*' }), (req, res) => {
const received = req.headers[HEADER];
if (!EXPECTED || typeof received !== 'string' || !safeEqual(received, EXPECTED)) {
return res.sendStatus(401);
}
const raw = req.body.toString('utf8');
let alert = raw;
try {
alert = JSON.parse(raw);
} catch {}
// Store the alert, return 200, and route it asynchronously.
res.sendStatus(200);
});
The same check in Python:
import hmac
import os
HEADER = os.environ.get("CLICKHOUSE_WEBHOOK_HEADER", "Authorization")
EXPECTED = os.environ.get("CLICKHOUSE_WEBHOOK_SECRET", "")
def verify_clickhouse_request(headers) -> bool:
if not EXPECTED:
return False # never accept when unconfigured
received = headers.get(HEADER) # use a case-insensitive headers object
if not received:
return False
# Compare bytes: compare_digest raises TypeError on a non-ASCII str.
return hmac.compare_digest(received.encode("utf-8"), EXPECTED.encode("utf-8"))
A secret header authenticates the sender, not the body. It cannot detect a modified alert, and with no signature or timestamp a captured request can be replayed. Use an HTTPS Webhook URL and a long random value, and if the value might have leaked, change it on the webhook and in your receiver together.
Make ClickHouse alerts production-ready. Hookdeck Event Gateway checks your secret header on every alert, records each request so you can see what ClickStack sent, and retries delivery to your service on your schedule.
ClickHouse webhook limitations and pain points
Repeated alerts and no recovery notice
The Problem: A persistent problem sends the same alert every interval, and nothing tells you when it has cleared.
Why It Happens: Each evaluation is independent. ClickStack notifies when the condition is met and sends nothing when it is not.
Workarounds:
- Track open alerts on your side and consider one resolved when no notification has arrived for a few intervals.
- Choose an interval that matches how often you want to hear about an ongoing problem.
How Hookdeck Can Help: Deduplication can drop repeats of the same alert within a time window, so your on-call tool gets one notification per incident rather than one per interval.
Delivery behaviour is undocumented
The Problem: If your endpoint is down when an alert fires, you do not know whether ClickStack will try again.
Why It Happens: ClickHouse does not document retries, timeouts, the method or the content type for Generic webhooks.
Workarounds:
- Keep the receiver simple and highly available, and return
200as soon as the request is stored. - Check the alert history in HyperDX, where red bars show when an alert fired, against what your receiver recorded.
How Hookdeck Can Help: Hookdeck accepts each alert and stores it before forwarding, then delivers to your service with retries you configure, and Issues notify you when deliveries fail.
Bursts from grouped alerts
The Problem: One alert can produce many requests at once.
Why It Happens: Grouped alerts send a notification for each group that breaches the threshold, so a widespread problem across many services can produce one request per service.
Workarounds:
- Group on a dimension with a bounded number of values.
- Make the receiver queue requests rather than process them inline.
How Hookdeck Can Help: A max delivery rate on your destination spreads the burst out, and filters can route alerts by fields in your template to different connections.
Static header authentication
The Problem: Anyone with the header value can send alerts your receiver will accept.
Why It Happens: Generic webhooks are not signed. The header you add is the only check.
Workarounds:
- Use HTTPS and a long random value, and change it if it might have leaked.
- Treat an alert as a prompt to look, and follow
{{link}}into ClickStack rather than acting on the body alone.
How Hookdeck Can Help: Hookdeck's ClickHouse source checks the header on every request. Hookdeck answers ClickStack with a 200 either way, but a request with a missing or wrong value is recorded as failed verification and never reaches your service.
Best practices
Put routing fields in the body template
ClickStack sends only what your template contains. Add fixed fields such as the alert's name, team or severity to each webhook's template, so your receiver can route and deduplicate without parsing free text. If several alerts share one webhook, consider separate webhooks per team or severity.
Keep the number of secrets small
Because a webhook can be reused across alerts, keep one secret header per webhook and one webhook per destination. That keeps the number of secrets to change small.
Write alert queries with time filters
For SQL alerts, include a time range filter such as $__timeFilter(col). Without one, ClickHouse notes the query reads the entire time range available in the source table, which makes evaluations slow, and slow evaluations are one of the reasons intervals get skipped.
Acknowledge fast and process asynchronously
Check the header, store the request, return 200, then notify people. See why to process webhooks asynchronously.
Conclusion
ClickStack's Generic webhooks turn ClickHouse searches and charts into alerts your own systems can act on. The request is shaped by your template, authenticated only by a header you add, and sent on every evaluation where the condition holds, with no recovery notice and no documented retry policy. Compare the header in constant time over HTTPS, put routing fields in the template, and plan for repeats and bursts.
Hookdeck Event Gateway checks the header, records every alert, removes repeats, and retries delivery to your service.
Get started with Hookdeck for free and handle ClickHouse alert webhooks reliably in minutes.