Lesson 4 of 4 · Claude API & Development

Pricing & Rate Limits

reading22 min

Story: The Startup That Ran Out of Tokens at 2 AM

DevAssist was a coding assistant startup. They launched their beta on a Tuesday and got featured on Hacker News. By midnight, 3,000 developers had signed up. By 2 AM, their API calls started failing with 429 (rate limit) errors. By 3 AM, their Anthropic account hit its spending limit and every request returned an error.

The founder, Elena, woke up to a Slack channel full of angry beta users and a $4,200 API bill for a single night. What went wrong?

Three things: (1) Elena hadn't configured spending limits, so costs spiraled unchecked. (2) She didn't implement rate limiting on her own API, so every user keystroke fired a new Claude request. (3) She used Opus for everything, including syntax highlighting suggestions that Haiku could handle at 1/20th the cost.

After that night, Elena built three systems: a cost estimation calculator, per-user rate limiting, and a model routing layer. Her next month cost $800 instead of $4,200 -- serving 5x more users.

This lesson teaches you everything Elena learned the hard way.


Concept: How Claude Pricing Works

Token-Based Pricing

Claude charges per token -- the fundamental unit of text processing. A token is roughly:

  • 4 characters in English
  • 0.75 words (or about 1.3 tokens per word)
  • A single code keyword (like function or const) is typically one token
  • Numbers can be multiple tokens: 1000 is one token, 123456789 might be three
Concept Card

Every API call has two token costs:

  • Input tokens: Everything you send (system prompt + messages + images)
  • Output tokens: Everything Claude generates in response
Total Cost = (input_tokens x input_price) + (output_tokens x output_price)

Current Pricing Table

ModelInput (per 1M tokens)Output (per 1M tokens)With Prompt Caching (Input)
Claude Opus 4$15.00$75.00$1.50 (cached)
Claude Sonnet 4$3.00$15.00$0.30 (cached)
Claude Haiku 3.5$0.80$4.00$0.08 (cached)

5x

Output vs Input Cost

Output tokens cost 5x more than input tokens across all models -- controlling response length is the single easiest cost optimization

Key observations:

  • Output tokens cost 5x more than input tokens across all models
  • Opus is 5x more expensive than Sonnet, which is ~4x more expensive than Haiku
  • Prompt caching reduces input costs by 90% for repeated system prompts

Understanding Token Counts

Before you can estimate costs, you need to understand where tokens go in a typical request:

Python
import anthropic

client = anthropic.Anthropic()

# This request shows where tokens are spent
response = client.messages.create(
    model="claude-sonnet-4-20250514",
    max_tokens=1024,
    system="You are a helpful coding assistant.",  # ~7 tokens
    messages=[
        {
            "role": "user",
            "content": "Write a Python function to reverse a string."
            # ~10 tokens
        }
    ],
)

# Check actual token usage
print(f"Input tokens: {response.usage.input_tokens}")
print(f"Output tokens: {response.usage.output_tokens}")

# Calculate cost (Sonnet pricing)
input_cost = (response.usage.input_tokens / 1_000_000) * 3.00
output_cost = (response.usage.output_tokens / 1_000_000) * 15.00
total_cost = input_cost + output_cost
print(f"Total cost: ${total_cost:.6f}")
typescript
import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic();

const response = await client.messages.create({
  model: "claude-sonnet-4-20250514",
  max_tokens: 1024,
  system: "You are a helpful coding assistant.",
  messages: [{
    role: "user",
    content: "Write a Python function to reverse a string."
  }]
});

console.log(`Input tokens: ${response.usage.input_tokens}`);
console.log(`Output tokens: ${response.usage.output_tokens}`);

const inputCost = (response.usage.input_tokens / 1_000_000) * 3.00;
const outputCost = (response.usage.output_tokens / 1_000_000) * 15.00;
console.log(`Total cost: $${(inputCost + outputCost).toFixed(6)}`);

The Hidden Token Costs

Many developers underestimate their token usage because they forget about several hidden costs:

1. System prompts count as input tokens A 500-word system prompt adds ~650 tokens to every single request. At 10,000 requests/day with Sonnet, that's 6.5M tokens/day x $3/M = $19.50/day just for the system prompt.

Tip

Use Pricing & Rate Limits in a low-risk branch or scratch project first. That keeps the lesson concrete without making your first attempt carry production pressure.

2. Conversation history accumulates In a multi-turn conversation, you resend the entire history with each request. A 20-message conversation might have 10,000 tokens of history -- all charged as input.

3. Tool definitions count as input tokens Each tool definition (JSON Schema) adds tokens. Five tool definitions might add 500-1000 input tokens per request.

4. Images are expensive Images are tokenized at approximately 1,600 tokens per 1 megapixel. A standard 1080p screenshot costs about 1,300 tokens.

Python
def estimate_conversation_cost(
    model: str,
    system_prompt_tokens: int,
    avg_user_message_tokens: int,
    avg_assistant_response_tokens: int,
    num_turns: int,
    tool_definition_tokens: int = 0
) -> dict:
    """Estimate the total cost of a multi-turn conversation."""

    pricing = {
        "claude-haiku-3-5-20241022": {"input": 0.80, "output": 4.00},
        "claude-sonnet-4-20250514": {"input": 3.00, "output": 15.00},
        "claude-opus-4-20250514": {"input": 15.00, "output": 75.00},
    }

    prices = pricing[model]
    total_input_tokens = 0
    total_output_tokens = 0

    for turn in range(1, num_turns + 1):
        # Each turn sends: system + tools + all history + new message
        history_tokens = (turn - 1) * (
            avg_user_message_tokens + avg_assistant_response_tokens
        )

        turn_input = (
            system_prompt_tokens
            + tool_definition_tokens
            + history_tokens
            + avg_user_message_tokens
        )

        total_input_tokens += turn_input
        total_output_tokens += avg_assistant_response_tokens

    input_cost = (total_input_tokens / 1_000_000) * prices["input"]
    output_cost = (total_output_tokens / 1_000_000) * prices["output"]

    return {
        "total_input_tokens": total_input_tokens,
        "total_output_tokens": total_output_tokens,
        "input_cost": round(input_cost, 4),
        "output_cost": round(output_cost, 4),
        "total_cost": round(input_cost + output_cost, 4),
        "cost_per_turn": round(
            (input_cost + output_cost) / num_turns, 4
        ),
    }

# Example: 10-turn customer support conversation on Sonnet
result = estimate_conversation_cost(
    model="claude-sonnet-4-20250514",
    system_prompt_tokens=500,
    avg_user_message_tokens=100,
    avg_assistant_response_tokens=300,
    num_turns=10,
    tool_definition_tokens=200
)

print(f"Total input tokens: {result['total_input_tokens']:,}")
print(f"Total output tokens: {result['total_output_tokens']:,}")
print(f"Total cost: ${result['total_cost']}")
print(f"Cost per turn: ${result['cost_per_turn']}")
# Notice how later turns cost much more due to history accumulation

Rate Limits

Anthropic enforces rate limits to ensure fair access and system stability. Rate limits operate on three dimensions:

DimensionWhat It Limits
Requests per minute (RPM)Number of API calls
Input tokens per minute (ITPM)Total input tokens across all requests
Output tokens per minute (OTPM)Total output tokens across all requests

Your effective rate limit is the most restrictive of these three. If your RPM is 1,000 but your ITPM allows only enough for 500 requests, you're effectively limited to 500.

Rate limit tiers scale with your usage and spending:

Measure the Pricing & Rate Limits Tradeoff

  1. Choose one task you repeat often.
  2. Run it with the model, cost, or performance setting discussed in this lesson.
  3. Record latency, quality, and cost so you can choose intentionally next time.
TierUsage RequirementRPMITPMOTPM
Tier 1 (Free trial)$05040,0008,000
Tier 2$40+ credited1,00080,00016,000
Tier 3$200+ credited2,000160,00032,000
Tier 4$400+ credited4,000400,00080,000

These are approximate values and may vary. Check console.anthropic.com for current limits.

Handling Rate Limits in Code

When you hit a rate limit, the API returns a 429 status code with a retry-after header. Here's how to handle it properly:

Python
import anthropic
import time
import random

client = anthropic.Anthropic()

def call_with_backoff(
    messages: list,
    model: str = "claude-sonnet-4-20250514",
    max_retries: int = 5,
    max_tokens: int = 1024
) -> anthropic.types.Message:
    """Make an API call with exponential backoff for rate limits."""

    for attempt in range(max_retries):
        try:
            return client.messages.create(
                model=model,
                max_tokens=max_tokens,
                messages=messages
            )
        except anthropic.RateLimitError as e:
            if attempt == max_retries - 1:
                raise  # Give up after max retries

            # Exponential backoff with jitter
            base_delay = 2 ** attempt  # 1, 2, 4, 8, 16 seconds
            jitter = random.uniform(0, base_delay * 0.5)
            delay = base_delay + jitter

            print(f"Rate limited. Waiting {delay:.1f}s "
                  f"(attempt {attempt + 1}/{max_retries})")
            time.sleep(delay)
        except anthropic.APIStatusError as e:
            if e.status_code == 529:  # Overloaded
                time.sleep(5)
                continue
            raise

# Usage
response = call_with_backoff(
    messages=[{"role": "user", "content": "Hello!"}]
)
typescript
import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic();

async function callWithBackoff(
  messages: Anthropic.MessageParam[],
  model = "claude-sonnet-4-20250514",
  maxRetries = 5,
  maxTokens = 1024
): Promise<Anthropic.Message> {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await client.messages.create({
        model,
        max_tokens: maxTokens,
        messages,
      });
    } catch (error) {
      if (error instanceof Anthropic.RateLimitError) {
        if (attempt === maxRetries - 1) throw error;

        const baseDelay = Math.pow(2, attempt);
        const jitter = Math.random() * baseDelay * 0.5;
        const delay = baseDelay + jitter;

        console.log(
          `Rate limited. Waiting ${delay.toFixed(1)}s ` +
          `(attempt ${attempt + 1}/${maxRetries})`
        );
        await new Promise(r => setTimeout(r, delay * 1000));
      } else if (
        error instanceof Anthropic.APIError &&
        error.status === 529
      ) {
        await new Promise(r => setTimeout(r, 5000));
      } else {
        throw error;
      }
    }
  }
  throw new Error("Max retries exceeded");
}

const response = await callWithBackoff([
  { role: "user", content: "Hello!" }
]);

Prompt Caching: The 90% Discount

Prompt caching is one of the most impactful cost optimization techniques. When you mark parts of your prompt as cacheable, Anthropic stores them server-side. Subsequent requests that include the same cached content pay only 10% of the normal input price.

What can be cached:

  • System prompts
  • Tool definitions
  • Large reference documents
  • Few-shot examples

Minimum cacheable size: 1,024 tokens (Sonnet/Opus) or 2,048 tokens (Haiku)

Python
import anthropic

client = anthropic.Anthropic()

# Using prompt caching with a large system prompt
large_system_prompt = """You are an expert legal analyst.

Here is the complete regulatory framework you must reference:

[imagine 2000+ tokens of legal reference material here]

When analyzing contracts, always check against sections
4.2, 7.1, and 12.5 of the framework above."""

response = client.messages.create(
    model="claude-sonnet-4-20250514",
    max_tokens=1024,
    system=[
        {
            "type": "text",
            "text": large_system_prompt,
            "cache_control": {"type": "ephemeral"}
        }
    ],
    messages=[{
        "role": "user",
        "content": "Analyze this clause: ..."
    }]
)

# Check cache performance
print(f"Cache creation tokens: "
      f"{response.usage.cache_creation_input_tokens}")
print(f"Cache read tokens: "
      f"{response.usage.cache_read_input_tokens}")
# First call: cache_creation > 0, cache_read = 0
# Subsequent calls: cache_creation = 0, cache_read > 0
typescript
import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic();

const largeSystemPrompt = `You are an expert legal analyst.

Here is the complete regulatory framework you must reference:

[imagine 2000+ tokens of legal reference material here]

When analyzing contracts, always check against sections
4.2, 7.1, and 12.5 of the framework above.`;

const response = await client.messages.create({
  model: "claude-sonnet-4-20250514",
  max_tokens: 1024,
  system: [
    {
      type: "text",
      text: largeSystemPrompt,
      cache_control: { type: "ephemeral" },
    }
  ],
  messages: [{
    role: "user",
    content: "Analyze this clause: ..."
  }]
});

console.log(`Cache creation: ${response.usage.cache_creation_input_tokens}`);
console.log(`Cache read: ${response.usage.cache_read_input_tokens}`);

Batch API: The 50% Discount

For non-time-sensitive workloads, the Batch API offers a 50% discount. You submit up to 10,000 requests at once, and results are delivered within 24 hours.

Quick Check

What is the main benefit of using Pricing & Rate Limits well in Claude Code?

Good for: Data processing, content generation, evaluation runs, classification at scale. Not good for: Real-time chat, interactive applications, anything user-facing.


Apply: Building a Cost Dashboard

Here's a practical cost tracking system you can integrate into any application:

Python
import anthropic
import json
from datetime import datetime, timezone
from dataclasses import dataclass, field, asdict
from typing import Optional

@dataclass
class APICallRecord:
    timestamp: str
    model: str
    task: str
    input_tokens: int
    output_tokens: int
    cache_read_tokens: int
    cache_creation_tokens: int
    cost_usd: float
    latency_ms: float

class CostTracker:
    """Track and analyze Claude API costs."""

    PRICING = {
        "claude-haiku-3-5-20241022": {
            "input": 0.80, "output": 4.00,
            "cache_read": 0.08, "cache_creation": 1.00,
        },
        "claude-sonnet-4-20250514": {
            "input": 3.00, "output": 15.00,
            "cache_read": 0.30, "cache_creation": 3.75,
        },
        "claude-opus-4-20250514": {
            "input": 15.00, "output": 75.00,
            "cache_read": 1.50, "cache_creation": 18.75,
        },
    }

    def __init__(self):
        self.records: list[APICallRecord] = []

    def calculate_cost(self, model: str,
                        usage: anthropic.types.Usage) -> float:
        """Calculate the cost of a single API call."""
        prices = self.PRICING.get(model, self.PRICING[
            "claude-sonnet-4-20250514"
        ])

        input_tokens = usage.input_tokens
        output_tokens = usage.output_tokens
        cache_read = getattr(
            usage, 'cache_read_input_tokens', 0
        ) or 0
        cache_creation = getattr(
            usage, 'cache_creation_input_tokens', 0
        ) or 0

        # Subtract cached tokens from regular input
        regular_input = input_tokens - cache_read - cache_creation

        cost = (
            (regular_input / 1_000_000) * prices["input"]
            + (output_tokens / 1_000_000) * prices["output"]
            + (cache_read / 1_000_000) * prices["cache_read"]
            + (cache_creation / 1_000_000) * prices["cache_creation"]
        )

        return round(cost, 6)

    def record(self, model: str, task: str,
               response: anthropic.types.Message,
               latency_ms: float):
        """Record an API call for tracking."""
        usage = response.usage
        cost = self.calculate_cost(model, usage)

        record = APICallRecord(
            timestamp=datetime.now(timezone.utc).isoformat(),
            model=model,
            task=task,
            input_tokens=usage.input_tokens,
            output_tokens=usage.output_tokens,
            cache_read_tokens=getattr(
                usage, 'cache_read_input_tokens', 0
            ) or 0,
            cache_creation_tokens=getattr(
                usage, 'cache_creation_input_tokens', 0
            ) or 0,
            cost_usd=cost,
            latency_ms=latency_ms,
        )
        self.records.append(record)
        return record

    def summary(self) -> dict:
        """Get a cost summary."""
        if not self.records:
            return {"total_cost": 0, "total_calls": 0}

        total_cost = sum(r.cost_usd for r in self.records)
        by_model = {}
        by_task = {}

        for r in self.records:
            by_model.setdefault(r.model, 0.0)
            by_model[r.model] += r.cost_usd
            by_task.setdefault(r.task, 0.0)
            by_task[r.task] += r.cost_usd

        return {
            "total_cost": round(total_cost, 4),
            "total_calls": len(self.records),
            "cost_by_model": {
                k: round(v, 4) for k, v in by_model.items()
            },
            "cost_by_task": {
                k: round(v, 4) for k, v in by_task.items()
            },
            "avg_cost_per_call": round(
                total_cost / len(self.records), 6
            ),
        }

    def alert_if_over_budget(
        self, daily_budget: float
    ) -> Optional[str]:
        """Check if spending exceeds daily budget."""
        today = datetime.now(timezone.utc).date().isoformat()
        today_cost = sum(
            r.cost_usd for r in self.records
            if r.timestamp.startswith(today)
        )

        if today_cost > daily_budget:
            return (
                f"ALERT: Daily spend ${today_cost:.2f} "
                f"exceeds budget ${daily_budget:.2f}"
            )
        elif today_cost > daily_budget * 0.8:
            return (
                f"WARNING: Daily spend ${today_cost:.2f} "
                f"is at {today_cost/daily_budget*100:.0f}% "
                f"of budget"
            )
        return None

# Usage
import time

tracker = CostTracker()
client = anthropic.Anthropic()

start = time.time()
response = client.messages.create(
    model="claude-sonnet-4-20250514",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Hello!"}]
)
latency = (time.time() - start) * 1000

record = tracker.record(
    "claude-sonnet-4-20250514",
    "greeting",
    response,
    latency
)
print(f"Cost: ${record.cost_usd}")
print(json.dumps(tracker.summary(), indent=2))

API Cost Management

Do

Build cost tracking into your application from day one and set spending limits before writing any code

Don't

Treat cost monitoring as an afterthought -- by the time you notice runaway spending, the bill is already large

Try This Now

  1. Integrate the CostTracker into an existing project and monitor costs for a week.
  2. Add a save_to_file() method that persists records to JSON for later analysis.
  3. Build a function that projects monthly costs based on the current day's spending rate.
  4. Add per-user tracking to identify your most expensive users.

Reflect: Managing Your API Budget

Tip

Set spending limits immediately. In the Anthropic Console, configure monthly spending limits before you write any code. It's the simplest safeguard against runaway costs, and it takes 30 seconds.

Warning

Output tokens are 5x more expensive than input tokens. The single most impactful cost optimization is controlling output length. Add explicit instructions like "Respond in under 200 words" or "Return only the JSON object, no explanation." This alone can cut costs 30-50%.

Cost Optimization Checklist

StrategySavingsEffort
Use the right model per task50-80%Medium
Enable prompt cachingUp to 90% on inputLow
Control output length30-50%Low
Trim conversation history20-40%Medium
Use Batch API for async work50%Low
Implement request deduplicationVariesMedium

The Spending Guardrails Every App Needs

  1. Console-level spending limit -- hard cap in the Anthropic dashboard
  2. Application-level daily budget -- your code stops calling the API when you hit the limit
  3. Per-user rate limiting -- prevent any single user from consuming disproportionate resources
  4. Cost alerting -- get notified when spending is trending above projections
  5. Model fallback -- if budget is tight, automatically downgrade to cheaper models

Key Takeaways

  • Claude charges per token (~4 characters) with separate input and output prices
  • Output tokens cost 5x more than input tokens -- controlling response length is the easiest cost win
  • Prompt caching reduces repeated input costs by 90% -- use it for system prompts and tool definitions
  • Rate limits operate on three dimensions: requests/minute, input tokens/minute, and output tokens/minute
  • Always implement exponential backoff with jitter for rate limit handling
  • Hidden token costs include conversation history (accumulates per turn), system prompts, tool definitions, and images
  • The Batch API offers 50% savings for non-real-time workloads
  • Build cost tracking into your application from day one -- not as an afterthought
  • Set spending limits in the Anthropic Console before writing any code
  • Multi-turn conversations get exponentially more expensive as history grows -- implement conversation trimming

Next chapter: You'll make your first API call to Claude -- from setting up your API key to sending requests and handling responses.