NullSpend Docs

Session Limits

Session limits cap how much a single agent conversation can spend, regardless of the overall budget. A runaway agent loop that stays under velocity limits can s

Session limits cap how much a single agent conversation can spend, regardless of the overall budget. A runaway agent loop that stays under velocity limits can still accumulate significant cost across a long session — session limits provide a per-conversation ceiling.

See Budgets for overall budget configuration.

How It Works

Request arrives with X-NullSpend-Session header


Lookup session spend in DO SQLite


currentSpend + estimate > sessionLimit?
  ├─ NO → Continue to velocity + budget checks


429 (no Retry-After) → agent should start a new session

If the X-NullSpend-Session header is absent, session limit enforcement is skipped entirely.

Configuration

Set this field when creating or updating a budget via the API:

FieldTypeRangeDefault
sessionLimitMicrodollarsinteger or null> 0null (disabled)

Setting sessionLimitMicrodollars to null disables session limit enforcement for that budget entity.

Setting the Session Header

Send the X-NullSpend-Session header with each request to identify the conversation:

TypeScript:

const response = await fetch("https://proxy.nullspend.dev/v1/chat/completions", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${apiKey}`,
    "X-NullSpend-Session": "task-042",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    model: "gpt-4o",
    messages: [{ role: "user", content: "Hello" }],
  }),
});

Python:

response = requests.post(
    "https://proxy.nullspend.dev/v1/chat/completions",
    headers={
        "Authorization": f"Bearer {api_key}",
        "X-NullSpend-Session": "task-042",
        "Content-Type": "application/json",
    },
    json={
        "model": "gpt-4o",
        "messages": [{"role": "user", "content": "Hello"}],
    },
)

Claude Agent SDK:

const options = withNullSpend({
  apiKey: "ns_live_sk_...",
  budgetSessionId: "task-042",  // NOT the SDK's conversation sessionId
});

The proxy rejects the request with 400 bad_request if the session ID exceeds 256 characters. Choose short, meaningful IDs.

Session Tracking

Session spend is tracked in the Durable Object's SQLite database:

ColumnTypeDescription
entity_keytextBudget entity (user:{id} or api_key:{id})
session_idtextYour session identifier
spendintegerCumulative spend in microdollars
request_countintegerNumber of requests in this session
last_seenintegerTimestamp of last request (ms)

Lifecycle:

  • Reservation: When a request is approved, the estimated cost is added to spend
  • Reconciliation: When the actual cost is known, the delta (actual - estimate) is applied: spend = MAX(0, spend + delta)
  • Expired reservations: If a reservation expires without reconciliation (crash/timeout), the DO alarm reverses the reservation from session spend
  • Cleanup: Sessions with last_seen older than 24 hours are deleted by the DO alarm

429 Response

When a session limit is exceeded, the proxy returns:

{
  "error": {
    "code": "session_limit_exceeded",
    "message": "Request blocked: session spend exceeds session limit. Start a new session.",
    "details": {
      "session_id": "conv_abc123",
      "session_spend_microdollars": 4800000,
      "session_limit_microdollars": 5000000
    }
  }
}

No Retry-After header. Unlike velocity limits, the session is done — retrying won't help. The agent should start a new session (new X-NullSpend-Session value) to continue.

Webhooks

session.limit_exceeded

Fires when a request is denied because the session spend cap is reached. Key fields in data.object:

FieldDescription
budget_entity_typeuser or api_key
budget_entity_idThe entity whose session limit was hit
session_idThe session that exceeded the limit
session_spend_microdollarsCurrent session spend at denial time
session_limit_microdollarsConfigured session limit
modelModel of the denied request
provideropenai or anthropic
blocked_atISO 8601 timestamp

See Event Types for the full JSON example.

Key Behaviors

  • No header = no enforcement. Session limits only apply when X-NullSpend-Session is present on the request.
  • Client-defined sessions. The proxy never creates, invalidates, or manages session IDs — your agent decides when to start a new session.
  • Independent of budget resets. Session spend does NOT reset when the budget period resets. A session that spans a daily reset carries its full cumulative spend.
  • Always strict. Session limits are hard caps regardless of the budget policy (warn does not apply).
  • 24-hour cleanup. Stale session data is automatically cleaned up after 24 hours of inactivity via the DO alarm.

Enforcement Order

The enforcement pipeline runs in this order:

  1. Period reset — if the budget period has elapsed, reset spend before any checks run
  2. Session limit — deny before touching velocity counters
  3. Velocity limit — sliding window + circuit breaker
  4. Budget exhaustion — is there enough budget remaining?
  5. Reservation — reserve estimated cost

Session is checked before velocity so that denied requests don't inflate velocity counters or affect budget accounting.

Example

Scenario: $5 session limit, agent conversation "task-042".

  1. Agent starts conversation "task-042", sending X-NullSpend-Session: task-042
  2. First 10 requests cost $0.45 each — session spend reaches $4.50
  3. Request 11 has an estimated cost of $0.60
  4. $4.50 + $0.60 = $5.10 > $5.00denied
  5. session.limit_exceeded webhook fires
  6. Agent receives 429 with session_limit_exceeded error
  7. Agent starts a new conversation with X-NullSpend-Session: task-043
  8. New session starts at $0 spend — requests resume

On this page