Skip to content
Backstop
Esc
↑↓navigate↵open⌘Jpreview
On this page

Budget alerts

A webhook when a budget crosses a threshold, so the person who owns the budget hears about it while there is still budget left.

A budget that nobody is told about is a budget that is discovered on the invoice. Backstop can send a webhook when a budget crosses a threshold, so the person who owns the budget hears about it while there is still budget left.

from backstop import BackstopConfig

config = BackstopConfig(
    webhook_endpoints=["https://hooks.example.com/backstop"],
    webhook_secret="shared-secret",         # signs the payload
    alert_tiers=[0.8, 0.95],                # fire at 80% and 95%
    alert_dedup_ttl=86400.0,                # do not repeat within 24h
    alert_project_horizon_calls=5,          # project 5 calls ahead
)

What fires, and when

An alert fires when a budget crosses a tier — a fraction of its ceiling. The default tiers are 80% and 95%.

alert_project_horizon_calls makes the alert look a little further ahead than the call that crossed the tier: the request that will cross the threshold is projected from the recent run rate, so the alert arrives with a few calls of headroom rather than none.


Dedup

The same alert does not fire twice inside alert_dedup_ttl (24 hours by default). Without it, a budget sitting at 95% would fire on every single request, and an endpoint that receives a webhook per request stops being readable.

The dedup window is per (tenant, tier), so one tenant crossing a threshold does not silence another tenant’s.

The manager is built once, per transport, and shared. It used to be rebuilt per request by the gateway, which defeated the dedup entirely: the same alert fired on every proxied request, because the manager that remembered what it had already fired was a new one every time.


The payload

The webhook body is JSON. It carries the tenant, the spend, the limit, the tier that was crossed, and the moment it happened:

{
  "tenant_id": "payments",
  "used": 4021.55,
  "limit": 5000.00,
  "tier": 0.8,
  "kind": "budget_crossed",
  "at": "2026-10-20T16:58:56.889577Z"
}

The kind field is what a receiver switches on. budget_crossed is the one above.


The signature

When webhook_secret is set, the request carries a signature header computed over the body with HMAC-SHA256, so the receiver can tell that the request came from this deployment and was not edited in flight:

X-Backstop-Signature: sha256=<hex digest>

The receiver recomputes it over the raw body and compares in constant time. A signature over a re-serialised body will not match, so read the body once, keep the bytes, and verify against those.


Delivery

Delivery is fire-and-forget. The alert is dispatched on a background thread, so the request path is never blocked by a slow or dead endpoint. That also means delivery is not guaranteed: a process that exits immediately after the request may not have sent the webhook yet.

The trade is deliberate. A budget guard that blocks a request because a webhook endpoint is slow is a guard that has become an outage, and the request it was protecting is exactly the one it just delayed.

Retries and a dead-letter queue are not implemented. If an endpoint needs at-least-once delivery, put a queue in front of it — that is a solved problem and not one a library should re-solve badly.


What it does not do

  • It does not block anything. An alert is information, not enforcement. A ceiling is what stops the spend.
  • It does not deliver exactly once. It delivers at-most-once per tier per dedup window, on a best-effort basis, from a background thread.
  • It does not retry. There is no dead-letter queue and no backoff. Put a queue in front of the endpoint if you need that.
  • It does not know your on-call. It posts to a URL. What happens next is yours.

Was this page helpful?