Production AI Guide

AI Cost Modeling: Tokens, Model Selection, and Budget Control

Measure token usage, price each category, and account for every attempt. This guide provides executable cost and quota examples, plus a way to separate model selection, cache savings, and application budgets.

Published: · Last reviewed: Oct 27 2026

Build a Cost Model from Measured Usage

Estimate the tokens and paid operations for one successful task, including failed attempts, retries, retrieval, and tool loops. Then model traffic and concurrency. A request-count limit controls call volume; it cannot enforce a dollar budget when request sizes vary.

Measure representative inputs with the selected model's token-counting API and compare them with final response usage. Character counts, word counts, and lines of code are rough planning aids, not billing measurements. Changing models or tokenizers requires new measurements. Prompts include system instructions, history, retrieved context, and tool definitions as well as the latest user message.

Choose Models against Quality and Cost Requirements

Compare active models on held-out task and safety cases, latency, failure rate, and total cost per successful task. A small exploratory sample can find problems, but no fixed number of cases proves that a cheaper model meets your requirements. Use explicit eval gates and document uncertainty. Escalation to another model adds another attempt to the task's cost.

Claude API standard global list prices checked Oct 27 2026, in USD per million tokens:

ModelInputOutput
Fable 5.1$10$50
Opus 5.5$4$20
Sonnet 5.5$2$10
Haiku 5.5: prompt up to 100,000 tokens$0.10$0.50
Haiku 5.5: prompt over 100,000 tokens$0.50$2.50

For Haiku 5.5, all input categories count toward the threshold, including cache reads and writes. Above it, the higher tier applies to the whole request, including output. Read the current pricing table before using these rates. Partner platforms, batch, fast mode, regional routing, and server-side tools can have different charges or modifiers.

Model suitability is task-specific: evaluate compact classification or extraction separately from open-ended generation. Do not treat a model-family label as a production default. For requests that need escalation, see model routing and cascade costs.

Cache a Stable Prefix and Verify the Result

Place a cache breakpoint at the end of content that stays identical across requests. A changing timestamp or user-specific suffix before that breakpoint prevents prefix reuse. Verify cache writes and reads in usage; a cache marker does not guarantee a hit. The named Sonnet and Haiku models require at least 512 cacheable tokens on the Claude API.

The default ephemeral TTL is five minutes and a cache hit refreshes it. A one-hour TTL uses ttl: '1h'. Writes cost 1.25 times base input for five minutes and twice base input for one hour. The rates used below include Sonnet 5.5 cache reads at $0.10 per million tokens. Recalculate the break-even point from your write rate, read rate, reuse count, and TTL; consider concurrent misses as well as repeated hits.

Save the modules below side by side. They use Node.js 24.13.0, @anthropic-ai/sdk@1.3.0, and redis@6.3.0. Install the two packages with npm install --save-exact @anthropic-ai/sdk@1.3.0 redis@6.3.0 and retain the lockfile. Importing these modules makes no network calls. To call Claude, create a client with createClient(), which reads ANTHROPIC_API_KEY, and pass it to summarize(client, stablePrefix, userMessage). Use synthetic inputs while testing.

import Anthropic from '@anthropic-ai/sdk';

export const createClient = (options = {}) => new Anthropic({
  ...options, maxRetries: 0, timeout: 10_000,
}); // Defaults to ANTHROPIC_API_KEY; never log credentials.

export async function summarize(client, stablePrefix, userMessage) {
  if (![stablePrefix, userMessage].every((s) => typeof s === 'string' && s.trim())) {
    throw new Error('Nonempty prompt required');
  }
  return client.messages.create({
    model: 'claude-sonnet-5-5',
    max_tokens: 1024,
    system: [{ type: 'text', text: stablePrefix, cache_control: { type: 'ephemeral' } }],
    messages: [{ role: 'user', content: userMessage }],
  });
}

The example disables automatic SDK retries and sets a ten-second timeout so hidden retries do not expand its attempt count. Provider failures propagate. If you add retries, cap them, meter each attempt, and keep uncertain outcomes for reconciliation; a timeout does not establish that nothing was billed. The prefix still needs to meet the model's caching requirements. The code does not manufacture cache hits.

Separate Output Limits from Completion

max_tokens limits generated tokens, including thinking where applicable. It does not guarantee a complete paragraph or valid JSON. The example's 1024-token limit is a workload choice to evaluate. A ceiling of 8192 offers 32 times the token allowance of 256; actual cost follows generated usage rather than automatically charging the ceiling.

Ask for concise output while preserving necessary qualifications. Inspect stop_reason before accepting the result. A max_tokens response can be billable and incomplete; record its usage even if the product rejects its text. Structured output also needs schema and business-rule validation. An automatic retry with more tokens requires a separate budget decision.

Price Separate Token Categories

For final Claude API usage, input_tokens is uncached input. Total input is uncached input plus cache reads plus cache creation. Cache creation is further split by TTL; do not charge its total again on top of that split. output_tokens already accounts for generated thinking: do not add visible-text estimates to it.

The following executable module supports only the two named models at standard global direct-API list prices. It rejects missing counts, incompatible modes, unknown models, and missing write breakdowns. Use a final non-streaming response or a stream's assembled final message, not partial usage events. It assumes no other pricing modifiers or paid server tools. It is a token-price estimate, not an invoice, credits calculation, or atomic budget ledger.

const count = (value) => {
  if (!Number.isSafeInteger(value) || value < 0) throw new Error('Invalid token count');
  return value;
};

// Dated list-price estimate: standard, global, direct Claude API only.
// USD per million tokens, checked 2026-10-27. Not an invoice or budget ledger.
export function priceUsage(message) {
  const u = message.usage;
  if (!u || typeof message.stop_reason !== 'string' || !message.stop_reason) {
    throw new Error('Final usage required');
  }
  if ((u.inference_geo !== undefined && u.inference_geo !== 'global') ||
      (u.service_tier !== undefined && u.service_tier !== 'standard') ||
      (u.server_tool_use && Object.values(u.server_tool_use).some((n) => n !== 0))) {
    throw new Error('Unsupported pricing mode');
  }
  const input = count(u.input_tokens), output = count(u.output_tokens);
  const read = count(u.cache_read_input_tokens ?? 0);
  const created = count(u.cache_creation_input_tokens ?? 0);
  const write5m = count(u.cache_creation?.ephemeral_5m_input_tokens ?? 0);
  const write1h = count(u.cache_creation?.ephemeral_1h_input_tokens ?? 0);
  if (!Number.isSafeInteger(write5m + write1h) || write5m + write1h !== created) {
    throw new Error('Cache-write breakdown required');
  }
  const totalInput = input + read + created;
  count(totalInput);
  let rates;
  if (message.model === 'claude-sonnet-5-5') rates = [2, 10, 0.10, 2.50, 4];
  else if (message.model === 'claude-haiku-5-5') {
    // All input categories count toward the threshold; the whole request changes tier.
    rates = totalInput > 100_000 ? [0.50, 2.50, 0.05, 0.625, 1] : [0.10, 0.50, 0.01, 0.125, 0.20];
  } else throw new Error('Unsupported model: review the rate card');
  const tokens = { input, output, read, write5m, write1h, totalInput };
  const usd = [input, output, read, write5m, write1h]
    .reduce((sum, n, i) => sum + n * rates[i] / 1_000_000, 0);
  return { model: message.model, rateDate: '2026-10-27', tokens, usd };
}

At Sonnet 5.5 rates, 100 uncached input tokens, 1000 cache-read tokens, 400 five-minute writes, 200 one-hour writes, and 50 output tokens cost an estimated $0.0026. Total input is 1700 tokens. Preserve full precision when aggregating estimates; format only for display. A production monetary ledger should use exact decimal or integer units with an explicit rounding policy.

Run scenarios for typical traffic, long inputs, simultaneous work, retries, and provider failure. Input-length percentiles are not automatically daily-spend percentiles. Sum every attempt and paid operation, then divide by successful tasks when comparing models. Reconcile estimates with the provider's usage and cost records; missing usage is unresolved, not zero.

Record Minimal Usage Metadata

The next module returns an allowlisted usage record without user IDs, prompts, answers, or thinking text. Keep any necessary tenant attribution separate under your application's access and retention policy. Persist usage before accepting or rejecting generated content; a rejected answer can still cost money. The text helper accepts only a completed summary with supported content types. It is not a policy or factuality grader.

import { priceUsage } from './costs.mjs';

export function usageRecord(message) {
  const priced = priceUsage(message);
  return { feature: 'document_summary', model: priced.model,
    rate_date: priced.rateDate, ...priced.tokens, estimated_usd: priced.usd };
}

export function summaryText(message) {
  if (message.stop_reason !== 'end_turn' || !Array.isArray(message.content) ||
      message.content.some((b) => !['text', 'thinking', 'redacted_thinking'].includes(b.type))) {
    throw new Error('Incomplete or unsupported summary');
  }
  const blocks = message.content.filter((b) => b.type === 'text');
  if (blocks.some((b) => typeof b.text !== 'string')) throw new Error('Invalid text block');
  const text = blocks.map((b) => b.text).join('\n');
  if (!text.trim()) throw new Error('No summary text');
  return text;
}

For one response, first persist usageRecord(message) through your accounting adapter, then call summaryText(message) if this product requires a completed summary. If pricing or persistence fails, mark the attempt unresolved and stop further automatic work until your policy resolves it. Do not silently report a zero-dollar successful task. See observability for tracing and exporter isolation; financial reconciliation is a separate responsibility.

Apply Spending Controls at the Right Level

The Claude Console supports monthly workspace spend caps and configurable alert thresholds under Workspaces → select workspace → Spend limits. Workspace limits cannot exceed the organization's limits. These are provider controls; application users sharing one API workspace do not automatically receive individual budgets.

Choose the cap and alert headroom from your risk, traffic, and operating process. A generic “twice expected spend” rule is not a guarantee. In the app, reserve a conservative amount atomically before admitting work, include reservations for concurrent requests and possible retries, and reconcile against observed usage afterward. Keep uncertain attempts reserved until reconciled. Define what happens on accounting outages, cap changes, and unavailable providers. This guide does not supply that distributed monetary ledger.

Use an Atomic Request Quota as an Additional Control

This example enforces a request-count window beginning with a principal's first request. It is not a calendar-hour quota or a dollar limit. Derive a bounded opaque principal ID from authenticated identity, not a user-supplied header. All workers must share the same limit and window configuration, and the same dedicated Redis key namespace.

Redis executes the script without other clients interleaving increment and expiry. It also repairs a pre-existing counter without TTL. Script atomicity is not rollback on a command error: any error or ambiguous client timeout must prevent the provider call. Protect the namespace from other writers and do not silently fall back to process-local counters.

-- KEYS[1]: bounded, authenticated principal key; ARGV: limit, window milliseconds
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
if not limit or limit < 1 or limit % 1 ~= 0 or
   not window or window < 1 or window % 1 ~= 0 then
  return redis.error_reply('Invalid quota settings')
end
local count = redis.call('INCR', KEYS[1])
if count == 1 or redis.call('PTTL', KEYS[1]) < 0 then
  redis.call('PEXPIRE', KEYS[1], window)
end
local allowed = 0
if count <= limit then allowed = 1 end
return {allowed, redis.call('PTTL', KEYS[1])}
import { readFileSync } from 'node:fs';
const script = readFileSync(new URL('./quota.lua', import.meta.url), 'utf8');

export async function checkQuota(client, principalId, limit, windowMs) {
  if (typeof principalId !== 'string' || !/^[A-Za-z0-9_-]{1,64}$/.test(principalId) ||
      ![limit, windowMs].every((n) => Number.isSafeInteger(n) && n > 0)) {
    throw new Error('Invalid quota request');
  }
  // Errors propagate: the caller must not invoke the provider after a quota error.
  const result = await client.eval(script, {
    keys: [`ai_calls:${principalId}`], arguments: [String(limit), String(windowMs)],
  });
  if (!Array.isArray(result) || result.length !== 2 ||
      ![0, 1].includes(result[0]) || !Number.isSafeInteger(result[1]) || result[1] < 0) {
    throw new Error('Invalid quota response');
  }
  return { allowed: result[0] === 1, retryAfterMs: result[0] === 1 ? 0 : result[1] };
}

Create and connect a node-redis client from your trusted Redis URL before calling checkQuota(client, principalId, limit, windowMs). Configure connection and command deadlines, handle client error events without logging credentials, disable the offline queue, and close the client during shutdown. On denial or exception, do not invoke the model. Do not automatically retry an ambiguously completed EVAL: it can consume another quota count. Denied attempts also increment this counter, while the original TTL remains unchanged. Size and monitor the authenticated-principal keyspace.

The Lua script uses one declared key and Redis INCR/PTTL/PEXPIRE/EVAL, available in Redis 2.6 and later. Deploy on a currently supported Redis release and test against your actual server and client configuration, including ACLs, reconnects, concurrency, and cluster routing. The local checks for this guide use an embedded Lua VM and Redis-command fixtures; they do not validate a live Redis deployment.

Before Shipping

Official Sources and Review Trigger

Sources checked Oct 27 2026. Review by Nov 10 2026, or immediately when a price, usage schema, model setting, SDK retry behavior, or Redis/client contract changes. The thresholds and app policies here are design choices to validate against your workload.

Related Guides

Claude API for Developers

API request and response contracts, streaming, tools, and cache integration.

AI Evals in Production

Measure quality and safety before choosing a lower-cost model.

Batch API vs Real-Time Calls

Evaluate asynchronous processing and its separate pricing and recovery contract.