Plane docs
Guides

Usage and spend caps

What was spent, and how to stop it being spent.

Every turn writes a session.usage event, and those are folded into rollups you can query. usage.* costs only read, so a client can render its own budget without holding control.

const period = { from: "2026-09-01", to: "2026-09-30" }

// The environment, with each cap beside its meter.
const usage = await client.usage.environment({ period, groupBy: "agent" })
usage.totals.turns
usage.totals.costUsd
usage.spend.tokensPerMonth

// Per key, with each key's own cap.
await client.usage.keys({ period })

// One session, and its sub-agents.
const tree = await client.usage.session({ id: sessionId })
tree.own
tree.descendantUsage

costUsd is a lower bound whenever unpricedCalls > 0. A model Plane's price table does not know contributes nothing rather than a guess. Cost is computed per model call, not from a turn's summed tokens: a turn that fell down a fallback ladder used two models at two prices.

Caps

await client.environments.update({
  slug: "dev",
  spend: { costUsdPerMonth: 50, tokensPerMonth: 20_000_000 }
})

await client.apiKeys.update({ id: keyId, spend: { costUsdPerMonth: 10 } })

A key's cap may not exceed its environment's (SpendCapExceedsParentError).

Caps are checked in exactly three places — sessions.create, sessions.send, and when an agent spawns a sub-agent — and nowhere else.

  • A running turn always finishes. Cutting one mid-flight would leave an open tool call and a half-written answer to save a few cents.
  • sessions.decide is never gated. Refusing an approval would strand a session in requires_action forever, which is worse than one turn of overage.
  • An environment with no caps never reads the rollup at all, so the check is free for everybody who has not asked for it.
try {
  await client.sessions.send({ id, message })
} catch (error) {
  if (error instanceof SpendLimitExceededError) {
    // `resetsAt` is the only thing that changes the answer. Retrying before it is a
    // retry that cannot succeed.
    console.warn(`${error.scope} out of ${error.metric} until ${error.resetsAt}`)
  }
}

It is a 402, not a 429. 429 means "slow down", and every client library retries it with backoff; a monthly cap is not a rate.

Sub-agents cannot be used to evade a budget

A session's descendantUsage is everything its tree spent, and limits.maxInputTokensPerSession is checked against the total on the root session. A session.usage event carries its descendants' usage too, and the rollup sums only own usage — counting both would bill every sub-agent turn twice.

Traces

Separately from cost, every turn writes spans.

const trace = await client.traces.forSession({ id: sessionId, includeDescendants: true })

The control plane renders that as a waterfall, with infrastructure spans hidden behind a toggle by default (an unfiltered turn is about seventy rows).

On this page