Budget Alerts and Notifications
AISIX Cloud budget alerts notify you when spend crosses a configured percentage of a budget limit. Use them to warn operators before a blocking budget starts rejecting traffic or to monitor a warn-only budget.
Prerequisites
Before starting, prepare the following:
- At least one existing budget.
- A webhook URL or Slack incoming webhook URL for the organization notification channel.
Configure Notification Channels
Notification channels are organization-level alert destinations, managed from the Notifications view. Every enabled channel receives every alert raised in the organization.
Two channel types are available:
- Webhook: The control plane sends each alert as a JSON
POSTto your URL. Use this to integrate with incident tooling, chat systems, or other systems that accept HTTP callbacks. A webhook channel can also customize the request body and add request headers. - Slack: Set a Slack incoming webhook URL to receive alerts as readable Slack messages.
Create and test a channel:
- Open Notifications, then select New channel.
- Enter a Name, select the channel Type, and enter its URL.
- Keep Enabled selected, then select Create channel.
- Select Test for the new channel and confirm that the destination receives the test notification.
A channel's URL is write-only. After creation, the control plane shows only a masked form such as https://hooks.slack.com/***. Editing a channel with the URL left empty keeps the stored URL.
Disabling a channel stops future deliveries to it. Deleting a channel keeps its past delivery records; deliveries still queued for it are marked failed.
Allow Private Network Destinations
You can save a channel whose URL resolves to a private, loopback, or link-local address, but test and alert deliveries to that URL fail by default. This prevents an operator-supplied URL on a shared control plane from reaching internal endpoints.
For an on-premises control plane whose webhook receiver or Slack proxy is on an intranet, set AISIX_CLOUD_NOTIFY_ALLOW_PRIVATE_URLS=true on the control plane API. The corresponding Helm chart value is api.notifyAllowPrivateURLs.
Configure Alert Thresholds
Each budget carries a list of alert thresholds, expressed as percentages of its limit. Configure thresholds for each budget:
- Open the Budgets view and select the budget to edit.
- In Alert thresholds (%), enter one or more whole-number percentages separated by commas. For example, use
80, 100to receive an early warning and a limit-reached alert. - Save the budget.
You can enter up to 20 unique thresholds from 1 through 200. If you leave the field empty, the control plane uses 80.
Each threshold fires once per budget period. When spend crosses 80% of a monthly budget, the control plane sends one alert to every enabled channel. It does not repeat the 80% alert during the same period, but another configured threshold can trigger its own alert. After the period resets, each threshold can fire again. For a per-member team budget (each member in a team), each member alerts independently.
For a blocking budget, thresholds above 100% are normally not reached because later requests are rejected at the limit. However, a request admitted below the limit can complete above it, and concurrent requests can also produce an overshoot. A threshold above 100% can fire when this occurs.
The control plane evaluates spend continuously. An alert is sent within roughly 30 seconds of usage crossing a threshold, even if no further traffic follows.
Webhook Payload
By default, webhook channels receive the alert as this JSON document:
{
"event": "budget_threshold",
"dedup_key": "budget_threshold:6f6d…:…:1782864000:80",
"org_id": "1f0c…",
"budget_id": "6f6d…",
"budget_name": "payments-team monthly",
"scope": "team",
"scope_ref": "9a2b…",
"subject_name": "payments-team",
"threshold_pct": 80,
"percent": 82.3,
"spent_cents": 8230,
"limit_cents": 10000,
"period": "month",
"period_start": "2026-07-01T00:00:00Z",
"triggered_at": "2026-07-21T09:00:00Z"
}
Deliveries are at-least-once: a retried or multi-channel alert can reach the same receiver more than once. dedup_key is identical across all deliveries of one firing. Use it to drop duplicates if your receiver must handle each alert only once. Slack channels receive the same information rendered as a text message.
The request Content-Type is always application/json.
Customize the Request Body
Set Body template on a webhook channel to send a body of your own shape instead of the default document. This is how you post alerts directly to a chat platform that expects its own message format. Leave the field empty to send the default document.
The template is a Go text/template. Write a variable as {{ .name }}. These names are available:
| Variable | Description |
|---|---|
.event | Event type, budget_threshold. |
.dedup_key | Identifier shared by every delivery of one firing. |
.org_id | Organization ID. |
.budget_id | Budget ID. |
.budget_name | Budget display name. |
.scope | Budget scope, such as org, team, or member. |
.scope_ref | ID of the scoped subject. |
.subject_name | Name of the scoped subject. |
.threshold_pct | Threshold that fired, as a whole-number percentage. |
.percent | Spend as a percentage of the limit. |
.spent_cents | Spend in cents. |
.limit_cents | Budget limit in cents. |
.period | Budget period, such as month. |
.period_start | Start of the current period, RFC 3339. |
.triggered_at | Time the alert fired, RFC 3339. |
.message | One-line human-readable summary of the alert. |
The first fifteen are the fields of the default document; .message is an extra convenience, for example Budget 'payments-team monthly' (team 'payments-team') reached 82.3% of its month limit: spent $82.30 of $100.00.
One function is available: json, which renders a value as a JSON literal, quoting and escaping a string. Use it for every value you embed:
{"msg": {{ .message | json }}}
Interpolating inside quotes — {"msg": "{{ .message }}"} — also renders, but avoid it: a budget or team name the organization chose can contain a quote or a backslash, and the template then renders something that is not JSON. That fails the delivery permanently, and the alert is lost.
The following rules apply to a template:
- The rendered output must be valid JSON, and at most 64 KiB.
- Referencing a name outside the table is an error, not an empty string.
range,template, andblockare rejected. Every variable is a single string or number, so there is nothing to iterate over and no second template to invoke.ifandwithare available.- The control plane renders the template against a sample alert when you save the channel, and rejects it if the result does not render or is not JSON.
- A template that renders at save time but fails on a real alert ends that delivery immediately — it is not retried.
- Unlike a header value, a template is not write-only: reads return it as written. Put credentials in request headers, never in the template body.
- Body template and Headers are accepted on webhook channels only. Changing an existing webhook channel to
slackwhile either is still set is rejected — clear them in the same request.
Chat Platform Examples
For a Feishu (Lark) custom bot:
{"msg_type": "text", "content": {"text": {{ .message | json }}}}
For a DingTalk custom robot:
{"msgtype": "text", "text": {"content": {{ .message | json }}}}
For a WeCom group robot:
{"msgtype": "text", "text": {"content": {{ .message | json }}}}
To build the text yourself instead of using .message:
{"msgtype": "text", "text": {"content": {{ printf "Budget %s reached %d%% (%.1f%%) of its %s limit" .budget_name .threshold_pct .percent .period | json }}}}
Add Request Headers
Set Headers on a webhook channel to send extra request headers with every delivery to it — a bearer token, a shared signing secret, or a tenant selector. This is the right place for a credential, because header values are write-only.
The following rules apply to headers:
- At most 16 headers.
- A name must be a valid HTTP header name of at most 128 characters. No two names may differ only in case.
- A value is at most 4096 characters and must not contain any control character — not only a line break.
Content-Type,Content-Length,Host,Transfer-Encoding, andConnectionare rejected, matched without regard to case. The control plane sets them itself.
Reading a channel returns the configured header names with every value replaced by ***, so a read-modify-write round-trip works unchanged: submitting *** for a name keeps the value already stored under it, matched without regard to case. Submitting *** for a name that has nothing stored is rejected, since saving it would leave *** as the header's value. Submitting an empty object removes every configured header.
Verify Deliveries
The Delivery log in the Notifications view records the alerts the organization actually raised. Each entry includes its channel, status (pending, delivered, or failed), attempt count, last error, and exact payload.
Failed sends are retried automatically with increasing delays (1 minute after the first failure, then 5 minutes, 15 minutes, 1 hour, 6 hours). After six failed attempts, the delivery is marked failed and remains in the log for inspection. A failure a retry cannot fix — a body template that does not render, for example — ends the delivery at once instead.
When a failed attempt received an HTTP response, the entry also carries that response's status code and body. The body is usually the only thing that explains a rejection the status code alone does not. It is kept as the destination sent it, up to 1 MiB, except that the credentials this channel sent — its URL, whole or as just the path and query, and any configured header value of at least 8 characters — are replaced by ***, because destinations commonly echo them back in an error message. Shorter header values are left alone, since replacing them would redact unrelated text.
The delivery list carries the first 8 KiB of that body; select Load full response on an entry to fetch the rest from GET /notification_deliveries/{delivery_id}. Nothing is stored for a successful delivery, or for an attempt that never reached a response, such as a timeout, a refused address, or a name that does not resolve.
A test send does not appear in the delivery log. It is synchronous and is not retried; its result is returned to the console directly, carrying the exact request body that was sent, plus the destination's status code and response body — masked the same way — whenever a response arrived, on success as well as on failure. Use it to confirm that a body template produces what you expect before relying on it.
When the deployment allows private destinations (AISIX_CLOUD_NOTIFY_ALLOW_PRIVATE_URLS), a test send returns the destination's response body to anyone who can edit notification channels. Take that into account before enabling it on a control plane with untrusted operators.
Manage Alerts with the AISIX Cloud Admin API
Notification channels are part of the AISIX Cloud Admin API:
GET /notification_channelsPOST /notification_channelsGET /notification_channels/{channel_id}PATCH /notification_channels/{channel_id}DELETE /notification_channels/{channel_id}POST /notification_channels/{channel_id}/test
Budget alert thresholds are the alert_thresholds field on the budget resource. See the AISIX Cloud Admin API Reference for request and response schemas.
Next Steps
Continue with Logging and Auditing to investigate the requests and control-plane changes behind budget behavior. To interpret the spend records that drive budget evaluation, see Usage Reporting.