Skip to content

Budgets and the weekly digest

A budget watches how much a scope spends in a day or a month. When the spend passes the amount, Spanlight sends a budget.exceeded notification to the budget’s channels, and a budget set to block also makes the gateway refuse further calls in that scope. Manage budgets under Budgets in a project.

Anyone who can see the project can read its budgets; only organization admins and owners can create, edit or delete them. A project can have up to 50, each with up to 10 channels, and budget names are unique within the project.

Before you start

  • The admin or owner role in the project. Every role can read budgets
  • The worker running, with ALERTS_EVALUATION_ENABLED on (the default). Budgets are evaluated once a minute
  • To be notified: a channel under Alerts, Channels
  • To block calls: traffic that goes through the gateway. A budget cannot stop calls sent straight from the SDK
  • For the weekly digest: an email provider (EMAIL_PROVIDER) set on the server
Scope scope_id Spend counted
project none All priced spans of the project
gateway_key The id of one of the project’s gateway keys Spans produced by calls through that key
user An end-user id, as sent in the x-spanlight-user header Spans of traces for that user
model A model name Spans of that model. gpt-4o also covers snapshot names such as gpt-4o-2024-08-06 and gpt-4o-latest, but not gpt-4o-mini
metadata key=value, such as customer_id=acme, split at the first =. The value is kept exactly as you type it Spans of traces whose metadata has that value under that key
trace_name A trace name, 1 to 256 characters (surrounding spaces are removed) Spans of traces with exactly that name

There are no budgets per project API key: blocking only ever happens in the gateway, and a key that sends traces with the SDK cannot be stopped.

A metadata key does not need to be promoted for a budget: the spend is read from the spans, not from the attribution rollups. Metadata belongs to the trace, so a key set late in a trace puts all of that trace’s spans in the period under the budget. In the dialog, Metadata offers the promoted keys and the keys seen in the last 7 days, or takes any valid key, and Trace name suggests the names seen in the last 7 days. A scope_id that does not fit its scope is refused with 422 UNKNOWN_SCOPE.

The period is a UTC calendar day (daily, resetting at 00:00 UTC) or a UTC calendar month (monthly, resetting at 00:00 UTC on the 1st). The amount is in US dollars, above zero, with at most four decimal places.

Spend is the cost of the priced spans in the scope since the period began. A span whose model has no known price adds nothing, so spend is a lower bound when unpriced models are in use (the Unpriced models list shows which). The spend of a scope that has spans but no priced span at all shows as “—”, never as $0.

A budget fires when spend is strictly above the amount. It resolves, sending alert.resolved, on the first evaluation that measures spend at or below the amount.

Action When the budget is exceeded
notify Sends the notification. Nothing is blocked.
block Sends the notification and makes the gateway answer 402 BUDGET_EXCEEDED to calls in the scope, with no call to the provider and no cost, until the budget is no longer exceeded. It is a soft limit (below). The budget’s row shows Blocking (soft).

The notification is the same for both. It names the budget, what was spent and the amount, and for a blocking budget it says when the period resets.

Blocking is a soft limit. The gateway does not add up spend on each call. It reads the state the alert worker writes, and the worker measures every budget’s spend once every 60 seconds. Calls made after the amount is crossed and before the next measurement still go through. On 2026-10-14 a budget of $0.002 started blocking 85.5 s after its amount was crossed, and its spend reached $0.0122, six times the amount. A fast stream of expensive calls overshoots further. For a limit that must hold, set the amount below it. The 402 message says so too: it ends with (soft limit, measured every 60 s).

  • Ingestion is never blocked. POST /v1/traces, the OTLP endpoint and the SDK keep recording whatever the budgets say. A budget limits what the gateway spends, not what the dashboard may know.
  • Only the gateway blocks. Calls that do not go through a gateway key are not stopped.
  • Metadata and trace name are matched from the call. The gateway reads the x-spanlight-metadata and x-spanlight-trace-name headers. A call that starts a new trace has the name the trace will get, "<surface> <model>" with the model your client asked for, such as chat_completions gpt-4.1-mini. A call that joins a trace (x-spanlight-trace-id) and lacks either header also uses the stored trace’s metadata and name, the header winning per key. That is best effort: until a span of the trace has been stored, there is nothing to read, so the first calls of a run may not match. Send the headers on every call to be sure.
  • Several budgets can match one call. The call is refused if any enabled blocking budget that matches its project, gateway key, user, model, metadata or trace name is exceeded; the message names the one with the lowest amount.
  • A stopped worker means a stale guard. If evaluation stops, a blocking budget keeps its last state: an exceeded one keeps blocking and one not yet exceeded keeps allowing.
  • Editing is immediate. Changing a firing budget’s amount, scope or period, disabling it, or deleting it resolves it at once and unblocks the scope; if it is still over the line it fires again within a minute.

A blocked call gets HTTP 402 in the error shape of the surface the client used, so the official SDKs raise their usual billing exceptions and do not retry. The message names the budget and what was spent (or, when the spend is not known, the limit that was reached). See Gateway error responses for the common fields.

OpenAI (/gw/v1/chat/completions, /gw/v1/responses):

{
"error": {
"message": "Blocked by a Spanlight budget: Budget \"Monthly cap\" is exhausted: $52.13 of $50.00 spent this month (soft limit, measured every 60 s).",
"type": "insufficient_quota",
"param": null,
"code": "insufficient_quota",
"spanlight_code": "BUDGET_EXCEEDED"
}
}

Anthropic (/gw/v1/messages):

{
"type": "error",
"error": {
"type": "billing_error",
"message": "Blocked by a Spanlight budget: Budget \"Monthly cap\" is exhausted: $52.13 of $50.00 spent this month (soft limit, measured every 60 s).",
"spanlight_code": "BUDGET_EXCEEDED"
},
"request_id": "4f2c0a7e9b1d4c3f8a5e6d7c8b9a0f1e"
}

Both carry the header X-Spanlight-Code: BUDGET_EXCEEDED. Match on spanlight_code, not on the message.

At 00:00 UTC on the first day of a new period, a blocking budget stops blocking, even before the next evaluation has resolved it. The first evaluation of the new period resolves the budget whatever the new spend is (spend of blocked or unpriced calls is not known), and it fires again only if the new period’s spend is already above the amount. The state of a budget in the first minute of a period shows “—” until it is evaluated.

A budget of $0.01 is the quickest way to see a block.

  1. Create a blocking budget on the project with the amount 0.01. A toast reads Created budget "…".
  2. Send calls through a gateway key until their priced cost passes $0.01.
  3. Wait for the next evaluation, up to a minute. Until then the spend reads “—”.
  4. The budget’s row now shows the spend as $… of $0.01 with a percent of 100 % or more, in the danger colour, and a Blocking (soft) badge next to the name.
  5. The next call through the key is refused with 402 and spanlight_code BUDGET_EXCEEDED.

Every Monday at 08:00 UTC, each organization member with a verified email address gets one message per project with a summary of the week that ended at 00:00 UTC that day (seven whole UTC days, compared with the seven before): spend, LLM calls, error rate and p95 latency with their changes, the top five models by cost, and how many alerts fired.

  • Turn it off per project with Weekly digest email in project settings (admins and owners). It is on by default.
  • Quiet projects are skipped. A project is included only when it received at least one span during the week.
  • It needs an email provider. Without one, the digest job ends as skipped and sends nothing. Operators can switch the digest off for the whole deployment with WEEKLY_DIGEST_ENABLED.
  • Unknown figures show “—” with no change, and a change against an unknown or zero earlier value reads “new”. The shared demo account and the demo organization are never mailed.

If a run is interrupted, the retry sends only to projects that were not yet queued, so nobody gets a project twice.

Budgets are evaluated by the alert evaluation job, so they follow ALERTS_EVALUATION_ENABLED; the digest follows WEEKLY_DIGEST_ENABLED. See Alerts and Configuration.

You see Check
A blocking budget did not block Evaluation lags up to a minute (see above). Check that the budget is enabled, that its scope matches the call (the gateway key id, the exact x-spanlight-user value, the model the client asked for or the route’s resolved model, the key=value pair in x-spanlight-metadata, or the trace name), and that spend is above the amount: a model without a known price adds nothing.
Spend shows “—” The budget has not been evaluated yet (it shows within a minute), or the scope has spans and none is priced. The tooltip on “—” says which: “Not evaluated yet. Spend shows within a minute.” or “No spend measured in this period yet. It shows within a minute if the scope has priced spend.”
Calls are blocked and you want them back now Raise the amount, disable the budget or delete it. The scope is unblocked straight away.
No digest arrived An email provider must be configured, the project’s toggle must be on, the project must have had traffic that week, and your email must be verified.
Evaluation is late or stopped See the alerts runbook, in particular evaluation lag.