Safety & Production · Production Deployment

Production Deployment

A demo only has to work once, for you, on a good network day. Production means the same Claude calls have to survive real traffic: picking where Claude runs, surviving rate limits gracefully, keeping cost under control at scale, and treating prompts like the code they are.

☺ Explain it like I'm 10

Cooking dinner for one friend, you can wing it — grab whatever's in the fridge, taste as you go. Running a restaurant that never closes is a different job: you need reliable suppliers, a plan for the Friday-night rush, a way to keep food costs from eating your profit, and a recipe book everyone on staff actually follows. Production deployment is that restaurant playbook for Claude — where you buy your "ingredients" (which API), what happens when you're slammed (rate limits), how you keep the bill sane (routing, caching, batching), and how you track every change to the menu (prompt versioning).

🦫Your host for this topic: Benny the Beaver (turns a working demo into a running system).

Where Claude runs: direct API, Bedrock, and Vertex AI

☺ Like you're 10: Same phone, different carriers — Claude is the phone, and the direct API, Bedrock, and Vertex AI are carriers you pick based on which one your family already has a plan with.

Every example so far in this course has hit api.anthropic.com directly. That's the fastest path to new models and the full feature set, but it isn't the only option. Claude is also available through Amazon Bedrock and Google Cloud's Vertex AI, plus a separate Anthropic-operated "Claude Platform on AWS" offering and Microsoft Foundry. Teams pick a cloud platform over the direct API for reasons that have nothing to do with the model itself: existing cloud spend commitments, a procurement process that already approved AWS or GCP as a vendor, IAM-based auth that plugs into infrastructure your security team already trusts, or data-residency requirements met by a specific cloud region.

Access pathWhy a team picks itWatch out for
Direct Anthropic APIFastest access to new models; full feature set (Batches, Admin API, Agent Skills, MCP connector, server-side tools)A separate vendor relationship and invoice outside your existing cloud spend
Amazon BedrockExisting AWS commitments and procurement; IAM auth; data stays inside your AWS compliance boundaryModel IDs are prefixed differently than the first-party API; a legacy page covers Opus 4.6-and-earlier models specifically
Google Cloud Vertex AIExisting GCP commitments; global, multi-region, or regional endpoints for data residencyMessage Batches API, Admin API, Agent Skills, the MCP connector, and server-side tools like code execution are not supported on Vertex

The Messages API shape is nearly identical across platforms, with two concrete differences on Vertex: the model goes in the endpoint URL rather than the request body, and the body needs anthropic_version set to the literal string vertex-2023-10-16 instead of the header-based versioning the direct API uses.

// Vertex AI request body — model is NOT a body field here,
// it's part of the endpoint path instead
{
  "anthropic_version": "vertex-2023-10-16",
  "max_tokens": 1000,
  "messages": [
    {"role": "user", "content": "Summarize this quarter's incident report."}
  ]
}
→ Tip

Vertex also offers global (no price premium), multi-region (10% premium), and regional (10% premium) endpoints — regional endpoints are the lever to reach for when a data-residency requirement, not raw throughput, is driving the decision.

Note the naming difference on AWS specifically: Bedrock exposes Claude under anthropic.-prefixed model IDs, while the separate Claude Platform on AWS and Microsoft Foundry offerings are Anthropic-operated and use the same first-party model IDs (like claude-opus-5) you'd use against the direct API. If you migrate between platforms, that's the first thing to check.

Handling rate limits

☺ Like you're 10: Think of a bouncer with three separate counters — how many people per minute, how many words coming in, how many words going out. Cross any one line and you're waiting outside, even if the other two are fine.

Rate limits are enforced per organization using a token-bucket algorithm across three dimensions per model: requests per minute (RPM), input tokens per minute (ITPM), and output tokens per minute (OTPM), scaled by your usage tier (Start, Build, Scale, or Custom). Only uncached input tokens count toward ITPM for most models — one more reason prompt caching, covered below, effectively raises your real throughput without needing a rate-limit increase at all.

When you exceed a limit, the API returns HTTP 429 with a rate_limit_error body. The response headers tell you exactly what to do next: a retry-after header gives you seconds to wait, and a family of anthropic-ratelimit-* headers reports the limit, remaining count, and reset time for requests, input tokens, and output tokens separately. Official guidance is to retry both 429 responses and 5xx (api_error, overloaded_error) responses with exponential backoff.

→ Tip

The official SDKs already do this for you: by default they retry transient failures — connection errors, 429s, 5xxs — twice, with backoff, honoring retry-after when present. Bump it with a client option before writing your own retry loop.

import anthropic

# Let the SDK handle retries — usually all you need
client = anthropic.Anthropic(max_retries=5)

For cases where you're calling the raw HTTP endpoint, running on a platform without SDK support, or want custom jitter behavior, here's a manual backoff loop that reads retry-after when it's present and falls back to exponential backoff with jitter when it isn't:

Python
import random
import time
import requests

def call_with_backoff(payload, headers, max_retries=6):
    for attempt in range(max_retries):
        response = requests.post(
            "https://api.anthropic.com/v1/messages",
            headers=headers,
            json=payload,
        )
        if response.status_code == 429:
            retry_after = response.headers.get("retry-after")
            if retry_after is not None:
                delay = float(retry_after)
            else:
                delay = min(60, 2 ** attempt) + random.uniform(0, 1)
            time.sleep(delay)
            continue
        response.raise_for_status()
        return response.json()
    raise RuntimeError("Exceeded max retries after repeated 429 responses")
JavaScript
async function callWithBackoff(payload, headers, maxRetries = 6) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    const response = await fetch("https://api.anthropic.com/v1/messages", {
      method: "POST",
      headers,
      body: JSON.stringify(payload),
    });
    if (response.status === 429) {
      const retryAfter = response.headers.get("retry-after");
      const delay = retryAfter
        ? Number(retryAfter) * 1000
        : Math.min(60000, 2 ** attempt * 1000) + Math.random() * 1000;
      await new Promise((resolve) => setTimeout(resolve, delay));
      continue;
    }
    if (!response.ok) throw new Error(`API error: ${response.status}`);
    return response.json();
  }
  throw new Error("Exceeded max retries after repeated 429 responses");
}
⚠ Careful

Anthropic also enforces separate "acceleration limits" triggered by sharp usage spikes, distinct from the steady-state token bucket. Ramp new traffic gradually — a sudden burst can get throttled even if you're well under your steady-state RPM/ITPM/OTPM limits.

🎬 At the Claude Crew
🦊

Foxy: Wait, I'm way under my requests-per-minute limit. Why did I just get throttled?

🦉

Professor Owl: Rate limits aren't the only speed bump. There's also an "acceleration limit" — a separate check for sudden spikes, on top of your steady-state RPM/ITPM/OTPM bucket.

🦉

Professor Owl: Jump from 10 requests a minute to 10,000 in one leap, and you can get throttled even with plenty of steady-state room left.

🦫

Benny the Beaver: So ramp traffic up gradually, like filling a dam a little at a time — and let the retry-after header, not a guess, decide how long to wait before pushing again.

Cost optimization

☺ Like you're 10: Three separate dials for your bill: use a cheaper model when the job is easy, remember old context instead of re-reading it every time, and batch up anything that isn't urgent.

Model routing

Not every request needs your most capable model. Official guidance frames model selection as a capability/speed/cost tradeoff, and recommends either starting cheap (e.g. Haiku) and upgrading only where quality falls short, or starting capable and optimizing down later once you know where quality margin exists. Third-party analyses report 40-80% cost reduction from routing by task complexity — a cheap classifier call decides whether the real request goes to a fast, inexpensive model or a slower, more capable one.

Support queryincoming request Classifycheap Haiku 4.5 call Routesimple vs. complex AnswerHaiku 4.5 or Opus 5
import anthropic

client = anthropic.Anthropic()

def classify_complexity(query: str) -> str:
    triage = client.messages.create(
        model="claude-haiku-4-5",
        max_tokens=10,
        messages=[{
            "role": "user",
            "content": f"Classify this support query as 'simple' or 'complex'. "
                        f"Reply with one word.\n\nQuery: {query}",
        }],
    )
    label = next(b.text for b in triage.content if b.type == "text")
    return "complex" if "complex" in label.strip().lower() else "simple"

def answer(query: str):
    model = "claude-opus-5" if classify_complexity(query) == "complex" else "claude-haiku-4-5"
    return client.messages.create(
        model=model,
        max_tokens=1000,
        messages=[{"role": "user", "content": query}],
    )

Prompt caching

When repeated calls share a long, stable prefix — a system prompt, a large reference document or codebase, a set of tool definitions, few-shot examples — mark a breakpoint with cache_control and let the API skip reprocessing that prefix on subsequent calls. A cache read costs roughly 10% of the base input price (a 90% saving on that portion of the prompt), while writing to the cache costs more up front: 1.25x base input price for the default 5-minute TTL, or 2x for a 1-hour TTL. The minimum cacheable prefix is 512-4096 tokens depending on model, and you get up to 4 cache breakpoints per request, checked up to 20 content blocks back.

{
  "model": "claude-sonnet-5",
  "max_tokens": 1024,
  "system": [
    {
      "type": "text",
      "text": "You are a support agent. Full product manual follows.\n\n<manual>...50,000 tokens of docs...</manual>",
      "cache_control": {"type": "ephemeral"}
    }
  ],
  "messages": [
    {"role": "user", "content": "How do I reset a device that won't power on?"}
  ]
}
OperationRelative cost
Regular (uncached) input tokens1x base input price
Cache write, 5-minute TTL (default)1.25x base input price
Cache write, 1-hour TTL (opt-in)2x base input price
Cache read (cache hit)~0.1x base input price

Batch processing

For workloads that don't need a synchronous response — nightly report generation, bulk classification, evaluation runs — the Message Batches API processes requests asynchronously at a flat 50% discount on both input and output tokens, across all models. Most batches complete within an hour, with a hard cutoff at 24 hours; a single batch can hold up to 100,000 requests or 256MB, and results stay retrievable for 29 days. Not every parameter is supported in batch mode — no streaming, no fast mode, no Threads, no cache-hint — so design batch requests as self-contained calls.

[
  {
    "custom_id": "ticket-4471",
    "params": {
      "model": "claude-haiku-4-5",
      "max_tokens": 500,
      "messages": [{"role": "user", "content": "Summarize support ticket #4471."}]
    }
  },
  {
    "custom_id": "ticket-4472",
    "params": {
      "model": "claude-haiku-4-5",
      "max_tokens": 500,
      "messages": [{"role": "user", "content": "Summarize support ticket #4472."}]
    }
  }
]
→ Tip

Batches can run well past the default 5-minute cache window, so if a batch's requests share a large common prefix, pair it with a 1-hour cache TTL rather than the default — otherwise every request in the batch pays the cache-write price instead of the cache-read price.

Logging, monitoring, and prompt versioning

The Usage and Cost Admin API exposes /v1/organizations/usage_report/messages and /v1/organizations/cost_report endpoints for token and cost tracking broken down by model, workspace, API key, and time bucket (1-minute, 1-hour, or 1-day granularity). It requires a separate sk-ant-admin... Admin key, data typically appears within about 5 minutes, and Anthropic recommends polling roughly once per minute for near-real-time dashboards. For deeper observability, official partner integrations include CloudZero, Datadog, Grafana Cloud, Honeycomb, and Vantage. If you're on Vertex AI, turn on Google's request-response logging service and retain logs on at least a 30-day rolling basis — that's Anthropic's own recommendation for monitoring activity and investigating misuse on that platform.

Prompt versioning is less standardized than you might expect. For the plain Messages API, there's no dedicated versioning product beyond Console-side prompt templates and variables (using the same {{PLACEHOLDER}} convention covered in the prompt-engineering module) plus a prompt generator/improver tool that tracks changes to prompt structure over time. The more formal mechanism lives in Claude's Managed Agents system: every agents.update call produces a new immutable version, and callers pin a specific version ID — so rolling a prompt forward or back is a config change, not a code deploy. Anthropic documents this directly in the "Managed Agents tutorial: prompt versioning and rollback" cookbook.

⚠ Careful

Whether or not you adopt Managed Agents, treat prompts as code regardless: store them in versioned files (plain text, JSON, or YAML with {{VARIABLE}} placeholders), commit changes to source control, and review prompt diffs the same way you'd review a code diff. A prompt change that silently regresses quality is exactly as dangerous as a code change that does, and just as easy to bisect if it's versioned.

Pattern vs. anti-pattern

◆ Pattern

A cheap classifier routes each request to Haiku 4.5 or Opus 5 based on complexity, the client is constructed with max_retries set (plus a manual backoff wrapper for any raw-HTTP fallback path), and every call's model, token usage, latency, and prompt version are logged to an observability backend — so a cost spike, a 429 storm, or a quality regression from a prompt edit all show up before customers notice.

⚠ Anti-pattern

model = "claude-opus-5" is hardcoded in a dozen call sites, a 429 crashes the request handler with no retry, and prompts live as inline strings edited directly in production with no diff, no version, and no log of who changed what or why.

✎ Try it yourself

Take your hello-world Messages API call and wrap it in the call_with_backoff function above. Add a log line before returning that records the model name, input/output token counts (from the response's usage field), and wall-clock latency for the call. Then add a one-line router: if the user's message is under 20 words, call claude-haiku-4-5; otherwise call claude-opus-5. Run both a short and a long prompt through it and confirm your log line shows the router picking a different model for each.

🦫 Benny the Beaver's checkpoint

You should now be able to explain where Claude can run beyond the direct API and why a team would pick each option, what a 429 response tells you and how to back off correctly, the three levers for cutting cost (routing, caching, batching) and roughly what each saves, and why prompts deserve the same version control as code. Next, see how these ideas combine into the broader patterns worth reusing across an agentic system on Patterns & Anti-patterns.

Check your answers
  1. Why might a team run Claude through Bedrock or Vertex AI instead of the direct API, and what's the tradeoff? Mainly for reasons unrelated to the model itself — existing AWS/GCP spend commitments, procurement that already approved that vendor, IAM-based auth, or data-residency needs. The tradeoff is feature gaps: Vertex doesn't support the Message Batches API, Admin API, Agent Skills, the MCP connector, or server-side tools, and Bedrock uses differently prefixed model IDs than the first-party API.
  2. What should you do when you get a 429, and what's an acceleration limit? Retry with exponential backoff, honoring the retry-after header when it's present (the official SDKs do this automatically via max_retries); also retry 5xx errors the same way. An acceleration limit is a separate throttle triggered by a sharp usage spike, distinct from your steady-state RPM/ITPM/OTPM bucket — so ramp new traffic up gradually rather than bursting.
  3. What are the three levers for cutting API cost, and roughly what does each save? Model routing sends easy requests to a cheaper model like Haiku 4.5 and only hard ones to Opus 5 (third-party reports put this at 40-80% savings); prompt caching with cache_control cuts the cost of a repeated stable prefix to about 10% of base price on a cache hit; and the Message Batches API gives a flat 50% discount on input and output tokens for asynchronous, non-latency-sensitive work.